diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ef773276 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,23 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +#* @global-owner1 @global-owner2 + +# Order is important; the last matching pattern takes the most +# precedence. When someone opens a pull request that only +# modifies JS files, only @js-owner and not the global +# owner(s) will be requested for a review. +#*.js @js-owner #This is an inline comment. + +# In this example, @octocat owns any file in an apps directory +# anywhere in your repository. +#apps/ @octocat + +pyproject.toml @margalva + +# define an owner for the CODEOWNERS file itself +/.github/ @margalva diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..7c6cfa25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,99 @@ +name: ๐Ÿž Bug +description: Fill a bug report here +title: "Bug located in ..." +type: "Bug" +labels: [""] +assignees: [""] + +body: + - type: textarea + id: bug-description + attributes: + label: '๐Ÿž Description of the bug' + placeholder: Describe what bug you encountered and what should have happened. + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: '๐Ÿ“ Steps to reproduce' + placeholder: Please write the steps to reproduce the issue. + validations: + required: true + + - type: textarea + id: installed-packages + attributes: + label: '๐Ÿ“ฆ Installed packages and configuration' + description: Describe the configuration (OS and packages that are being used) + placeholder: "Example : OS, package-lock.json..." + validations: + required: true + + - type: dropdown + id: severity + attributes: + label: '๐Ÿšฆ Defines the severity of the bug. In the case of class-3, [specific information must be added](https://github.com/ansys-internal/QP-2-Template/blob/main/.github/ISSUE_TEMPLATE/class3.md)' + options: + - "Class 1 - Crash/Major data loss" + - "Class 2 - Serious problem" + - "Class 2 - Minor problem" + - "Class 3 - Hidden error" + default: 0 + validations: + required: true + + - type: input + id: support-ticket-ID + attributes: + label: '๐Ÿ“ฆ Support ticket ID' + description: Give the CRM ticket ID if it's not an internal defect. + placeholder: + validations: + required: false + + - type: input + id: found-in + attributes: + label: '๐Ÿž Found in' + description: Give the version number the bug has been found. + placeholder: + validations: + required: false + + - type: input + id: fixed-in + attributes: + label: '๐Ÿฉน Fixed in' + description: Give the version number the bug has been fixed. + placeholder: + validations: + required: false + + - type: textarea + id: root-cause + attributes: + label: 'Root cause' + description: Provide the cause of the bug. + placeholder: + validations: + required: false + + - type: checkboxes + id: third-party + attributes: + label: Third Party + description: + options: + - label: This bug is introduced by a third party + + - type: checkboxes + id: bug-released + attributes: + label: Released Bug + description: + options: + - label: This bug is in a released product + + diff --git a/.github/ISSUE_TEMPLATE/class3.md b/.github/ISSUE_TEMPLATE/class3.md new file mode 100644 index 00000000..26e2593d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/class3.md @@ -0,0 +1,48 @@ +> This section SHALL be added at the end of a Released Class 3 bug. + +### Class 3 Only - Description + +> Description of a bug for the customer audience + +### Class 3 Only - Suggested User Action + +> Suggest what the end user should do to avoid the bug. + +### Class 3 Only - Defect ID (Int) + + +### Class 3 Only - Management Reviewer + + +### Class 3 Only - Management Review Date (datetime) + +> Write with this date format mm/dd/YY + +### Class 3 Only - Doc Reviewer + + +### Class 3 Only - Doc Review Date (datetime) + +> Write with this date format mm/dd/YYYY + +### Class 3 Only - Technical Reviewer + + +### Class 3 Only - Technical Review Date (datetime) + +> Write with this date format mm/dd/YYYY + +### Class 3 Only - Affected Program + + +### Class 3 Only - Affected Physics Area + + +### Class 3 Only - Keywords + + +### Class 3 Only - First Incorrect Version + + +### Class 3 Only - Corrected In Version + diff --git a/.github/ISSUE_TEMPLATE/concern.yml b/.github/ISSUE_TEMPLATE/concern.yml new file mode 100644 index 00000000..7e3c857b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/concern.yml @@ -0,0 +1,14 @@ +name: โš ๏ธ Concern +description: Describe the concern here +title: "" +labels: ["concern"] +assignees: [""] + +body: + - type: textarea + id: concern-description + attributes: + label: 'โš ๏ธ Description of the concern' + placeholder: Describe the concern and what should be done to resolve it, if possible. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000..ccdb0c99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,39 @@ +name: ๐Ÿ’ก New feature +description: features should define complete functionality that is testable and capable of adding value for the customer when shipped +title: "Add ..." +labels: [""] +assignees: [""] +type: "Feature" + +body: + + - type: textarea + id: feature-description + attributes: + label: '๐Ÿ“ Description of the feature' + placeholder: Describe the feature and why it is useful for the project + validations: + required: true + + - type: textarea + id: acceptanceCriteria + attributes: + label: Acceptance Criteria + description: List the criteria that must be met for the story to be considered complete. + placeholder: "- Given [context], when [action], then [outcome]" + validations: + required: true + + - type: textarea + id: business-value + attributes: + label: '๐Ÿ’ต Business Value' + validations: + required: false + + - type: textarea + id: references + attributes: + label: '๐Ÿ”— Useful links and references' + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/story.yaml b/.github/ISSUE_TEMPLATE/story.yaml new file mode 100644 index 00000000..ea97b2f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/story.yaml @@ -0,0 +1,54 @@ +name: ๐Ÿ“– User Story +description: Stories are pieces of work done in one iteration, they can be of several types, technical, functional... +title: "[User Story]: " +type: "Story" +labels: [""] + +body: + - type: textarea + id: description + attributes: + label: User Story Description + description: Describe the user story in detail, including the desired outcome. + placeholder: "As a [type of user], I want [an action] so that [a benefit/a value]" + validations: + required: true + + - type: textarea + id: acceptanceCriteria + attributes: + label: Acceptance Criteria + description: List the criteria that must be met for the story to be considered complete. + placeholder: "- Given [context], when [action], then [outcome]" + validations: + required: true + + - type: textarea + id: notes + attributes: + label: Additional Links + description: Links to Test Cases / Features / Other User Stories + placeholder: "[Issue #1](https://github.com/ansys-internal/QP-2-Template)" + validations: + required: false + + - type: dropdown + id: third-party + attributes: + label: Requires new or update to third party ? If "yes" add the name(s) and the version(s) of the Third Party Software. + options: + - "No" + - "Yes, new" + - "Yes, update" + default: 0 + validations: + required: true + + - type: textarea + id: third-party-desc + attributes: + label: Name and Version for Third Parties + description: Name and version of the integrated third party software components + placeholder: + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/test-case.yml b/.github/ISSUE_TEMPLATE/test-case.yml new file mode 100644 index 00000000..8497c559 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test-case.yml @@ -0,0 +1,48 @@ +name: ๐Ÿงช Test Case +description: Description of a test scenario covering a story or a feature. +title: 'Add...' +labels: [""] +assignees: [""] +type: "Test Case" + +body: + - type: textarea + id: description + attributes: + label: "๐Ÿ“ Description" + description: "Provide a clear and concise description of the test case." + placeholder: "Enter the description here" + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: "๐Ÿ“œ Steps" + description: "List the detailed actions to reproduce the test scenario." + placeholder: "1. Step 1..." + validations: + required: true + + - type: textarea + id: expected-results + attributes: + label: "๐Ÿงช Expected Results" + description: "Describe the expected outcome or behavior at the end of the test case." + placeholder: "Enter the expected results here" + validations: + required: true + + - type: textarea + id: test-data + attributes: + label: "๐Ÿ“ Test Data" + description: "Mention any specific data or inputs required for this test case." + placeholder: "- Data 1..." + + - type: textarea + id: related-issues + attributes: + label: "๐Ÿ”— Related Issues" + description: "Link to any related stories, features or pull requests, if applicable." + placeholder: "- Issue 1..." diff --git a/.github/ISSUE_TEMPLATE/test-log.yml b/.github/ISSUE_TEMPLATE/test-log.yml new file mode 100644 index 00000000..89a5d782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test-log.yml @@ -0,0 +1,51 @@ +name: ๐Ÿ“Š Test Log +description: "Template for logging the results of a manually executed test case." +title: "[Test Log]: <Test Name>" +labels: [""] +assignees: [""] +type: "Test Log" + +body: + - type: input + id: related-issues + attributes: + label: "๐Ÿ”— Related Test Case" + description: "Link to the Test Case this manual result is created from." + placeholder: "- Issue 1..." + validations: + required: true + + - type: textarea + id: test-results + attributes: + label: "๐Ÿ“Š Test Results" + description: "Log the results of each test step in the form of a table. Use `|` to separate columns and `-` for table headers." + placeholder: "| Test Step | Expected Result | Actual Result | Status |\n|-----------|-----------------|---------------|--------|\n| Step 1 | Expected 1 | Actual 1 | Pass |\n| Step 2 | Expected 2 | Actual 2 | Fail |" + validations: + required: true + + - type: textarea + id: build-name + attributes: + label: "Build Tested" + description: "Name/Number of the build the test has been executed" + placeholder: "Enter the build number here and configuration information" + validations: + required: true + + - type: textarea + id: platform + attributes: + label: "Platform" + description: "OS and version, Browser and version, Job scheduler and versions as relevant" + placeholder: "Example : Windows Server 22/Red Hat 8.6...Chrome v117" + validations: + required: true + + - type: checkboxes + id: overall-test-passed + attributes: + label: Overall result + description: + options: + - label: Test passed diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2c3acea6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,130 @@ +version: 2 + +registries: + github: + type: git + url: https://github.com + username: x-access-token + password: ${{ secrets.WORKFLOW_TOKEN }} + +updates: + - package-ecosystem: "github-actions" + directory: "/" + registries: + - github + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: + - "maintenance" + - "dependencies" + commit-message: + prefix: "chore(actions):" + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: + - "maintenance" + - "dependencies" + commit-message: + prefix: "build(deps):" + groups: + python-dependencies: + patterns: + - "*" + + - package-ecosystem: "npm" + directory: "/src/ansys/visor/visor-client" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 3 + labels: + - "maintenance" + - "dependencies" + commit-message: + prefix: "build(npm):" + groups: + visor-client: + patterns: + - "*" + + - package-ecosystem: "npm" + directory: "/src/ansys/visor/dash" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 3 + labels: + - "maintenance" + - "dependencies" + commit-message: + prefix: "build(dash):" + groups: + dash: + patterns: + - "*" + + - package-ecosystem: "docker" + directory: "/deploy/docker/dev" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 2 + labels: + - "maintenance" + - "dependencies" + commit-message: + prefix: "build(docker):" + groups: + docker-dev: + patterns: + - "*" + + - package-ecosystem: "docker" + directory: "/deploy/docker/fuji" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 2 + labels: + - "maintenance" + - "dependencies" + commit-message: + prefix: "build(docker):" + groups: + docker-fuji: + patterns: + - "*" + + - package-ecosystem: "docker" + directory: "/deploy/docker/release" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 2 + labels: + - "maintenance" + - "dependencies" + commit-message: + prefix: "build(docker):" + groups: + docker-release: + patterns: + - "*" \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..51b69ad2 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,52 @@ +documentation: + - head-branch: + - '^doc([/-]|$)' + - changed-files: + - any-glob-to-any-file: + - doc/source/**/* + - README.md + +maintenance: + - head-branch: + - '^maint([/-]|$)' + - '^ci([/-]|$)' + - '^no-ci([/-]|$)' + - changed-files: + - any-glob-to-any-file: + - .github/**/* + - pyproject.toml + - .pre-commit-config.yaml + +added: + - head-branch: + - '^feat([/-]|$)' + +fixed: + - head-branch: + - '^fix([/-]|$)' + - '^patch([/-]|$)' + +test: + - head-branch: + - '^test([/-]|$)' + - '^testing([/-]|$)' + - changed-files: + - any-glob-to-any-file: + - tests/** + +dependencies: + - changed-files: + - any-glob-to-any-file: + - package.json + - package-lock.json + - yarn.lock + - pnpm-lock.yaml + +breaking: + - head-branch: + - '^breaking([/-]|$)' + +miscellaneous: + - head-branch: + - '^chore([/-]|$)' + - '^junk([/-]|$)' \ No newline at end of file diff --git a/.github/scripts/check_junit_results.sh b/.github/scripts/check_junit_results.sh new file mode 100644 index 00000000..04f8e712 --- /dev/null +++ b/.github/scripts/check_junit_results.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# check_junit_results.sh โ€” Reusable JUnit XML result checker for CI workflows. +# +# Usage: +# check_junit_results.sh <suite_name>:<xml_path> [<suite_name>:<xml_path> ...] +# +# Environment variables: +# MISSING_XML_MODE What to do when an XML file is missing. +# "skip" = ignore that suite (default) +# "error" = treat as an error (exit 2) +# "fail" = treat as a failure (exit 1) +# DETECT_ERRORS Whether to detect <testsuite errors="N">. +# "true" = detect errors, use exit codes 0/1/2/3 (default) +# "false" = only detect failures, use exit codes 0/1 +# +# Exit codes: +# 0 = all suites passed +# 1 = at least one suite has test FAILURES (assertion failures) +# 2 = at least one suite has test ERRORS (infrastructure issues) +# 3 = both FAILURES and ERRORS detected across suites +# +# Note: Argument paths must not contain spaces. +# Note: Requires GNU grep (supports -oP). +# Note: This script is designed to work with xml with single suite per file, which is the default for pytest's junitxml output. + +set -euo pipefail + +# --- Guard: require GNU grep with -P support --- +if ! echo "" | grep -oP "" 2>/dev/null; then + echo "::error::This script requires GNU grep with Perl-compatible regex (-oP). Aborting." >&2 + exit 1 +fi + +# --- Read environment variables with defaults --- +MISSING_XML_MODE="${MISSING_XML_MODE:-skip}" # skip | error | fail +DETECT_ERRORS="${DETECT_ERRORS:-true}" # true | false + +# --- Validate at least one argument --- +if [ $# -eq 0 ]; then + echo "Usage: $0 <suite_name>:<xml_path> [<suite_name>:<xml_path> ...]" >&2 + echo "Example: $0 unit:tests/artifacts/unit/unit-junit.xml smoke:tests/artifacts/smoke/reports/smoke-junit.xml" >&2 + exit 1 +fi + +# --- Initialize accumulators --- +any_failed=0 +any_errored=0 +failed_suites="" +errored_suites="" + +# --- Process each suite argument --- +for arg in "$@"; do + suite_name="${arg%%:*}" + xml_path="${arg#*:}" + + # Handle missing XML file + if [ ! -f "$xml_path" ]; then + echo "โš ๏ธ [$suite_name] JUnit XML not found: $xml_path" + if [ "$MISSING_XML_MODE" = "skip" ]; then + echo " Skipping this suite." + continue + elif [ "$MISSING_XML_MODE" = "error" ]; then + echo " Treating as an error condition (MISSING_XML_MODE=error)." + any_errored=1 + errored_suites="${errored_suites}${suite_name}," + continue + else + # "fail" (default fallback) + echo " Treating as a failure (MISSING_XML_MODE=fail)." + any_failed=1 + failed_suites="${failed_suites}${suite_name}," + continue + fi + fi + + # Extract failure count from the <testsuite> element + failure_count=$(grep -oP 'failures="\K[0-9]+' "$xml_path" | head -1) + failure_count=${failure_count:-0} + + # Extract error count (only if DETECT_ERRORS=true) + if [ "$DETECT_ERRORS" = "true" ]; then + error_count=$(grep -oP 'errors="\K[0-9]+' "$xml_path" | head -1) + error_count=${error_count:-0} + else + error_count=0 + fi + + echo "๐Ÿ“‹ [$suite_name] tests โ€” failures=$failure_count, errors=$error_count" + + # Record failures + if [ "$failure_count" -gt 0 ]; then + echo " โŒ $failure_count test failure(s) detected." + any_failed=1 + failed_suites="${failed_suites}${suite_name}," + fi + + # Record errors + if [ "$error_count" -gt 0 ]; then + echo " โš ๏ธ $error_count test error(s) detected." + any_errored=1 + errored_suites="${errored_suites}${suite_name}," + fi +done + +# --- Remove trailing commas --- +failed_suites="${failed_suites%,}" +errored_suites="${errored_suites%,}" + +# --- Compute exit code and emit annotations --- +exit_code=0 + +if [ $any_failed -ne 0 ] && [ $any_errored -ne 0 ]; then + echo "" + echo "::error::Both test FAILURES and ERRORS detected." + exit_code=3 +elif [ $any_failed -ne 0 ]; then + echo "" + echo "::error::Test FAILURES detected." + exit_code=1 +elif [ $any_errored -ne 0 ]; then + echo "" + echo "::error::Test ERRORS detected (infrastructure/runtime issues)." + exit_code=2 +else + echo "" + echo "โœ… All test suites passed." +fi + +# --- Write outputs to $GITHUB_OUTPUT (guarded so the script also works locally) --- +if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" + echo "failed_suites=$failed_suites" >> "$GITHUB_OUTPUT" + echo "errored_suites=$errored_suites" >> "$GITHUB_OUTPUT" +fi + +exit $exit_code diff --git a/.github/workflows/build_and_package.yml b/.github/workflows/build_and_package.yml new file mode 100644 index 00000000..7cd6d34b --- /dev/null +++ b/.github/workflows/build_and_package.yml @@ -0,0 +1,363 @@ +name: Build and Release Package + +on: + schedule: # UTC at 22:00 = 6pm EDT + - cron: '0 22 * * *' + pull_request: + branches: + - main + push: + tags: + - 'v*' + branches: + - main +env: + MAIN_PYTHON_VERSION: "3.11" + POETRY_VERSION: "2.3.2" + + # For Solutions private PyPI: + POETRY_HTTP_BASIC_SOLUTIONS_PRIVATE_PYPI_USERNAME: "PAT" + POETRY_HTTP_BASIC_SOLUTIONS_PRIVATE_PYPI_PASSWORD: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_PASSWORD }} + + # For PyAnsys private PyPI: + POETRY_HTTP_BASIC_PYANSYS_PRIVATE_PYPI_USERNAME: "PAT" + POETRY_HTTP_BASIC_PYANSYS_PRIVATE_PYPI_PASSWORD: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_PASSWORD }} + +jobs: + update-changelog: + name: "Update CHANGELOG (on release)" + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + runs-on: ubuntu-latest + permissions: + contents: write # Required to create the changelog on the release branch + pull-requests: write # Required to create on main the associated PR with the changelog + steps: + - uses: ansys/actions/doc-deploy-changelog@84b6533ae76f5dde69a1df26c18682160a8121cf # v10.3.5 + with: + token: ${{ secrets.PYANSYS_CI_BOT_TOKEN }} + bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} + bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} + + pre-commit: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: '22.12.0' + + - name: Install visor-client npm dependencies + working-directory: src/ansys/visor/visor-client + run: npm install + + - name: Install dash npm dependencies + working-directory: src/ansys/visor/dash + run: npm install + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files + + build: + needs: [pre-commit, update-changelog] + if: > + always() && + (needs.pre-commit.result == 'success' || needs.pre-commit.result == 'skipped') && + (needs.update-changelog.result == 'success' || needs.update-changelog.result == 'skipped') && + (github.event_name != 'schedule' || github.ref == 'refs/heads/main') + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up Python + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential libx11-6 libxrender1 curl + + - name: Install poetry + run: | + pip install poetry==${{ env.POETRY_VERSION }} + poetry --version + + - name: Install dependencies for visor-setup + run: | + python -m venv .venv + poetry lock + poetry install --with setup --no-ansi --no-interaction + + - name: Run python setup file + run: | + if [[ "${{ runner.os }}" == "Windows" ]]; then . .venv/Scripts/activate; fi + if [[ "${{ runner.os }}" == "Linux" ]]; then . .venv/bin/activate; fi + visor-setup + + # Node.js package build + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: '22.12.0' + + - name: Install visor-client npm dependencies + working-directory: src/ansys/visor/visor-client + run: npm install + + - name: Build visor-client npm package + working-directory: src/ansys/visor/visor-client + run: npm run build + + - name: Install dash npm dependencies + working-directory: src/ansys/visor/dash + run: npm install + + - name: Build dash npm package + working-directory: src/ansys/visor/dash + run: | + poetry run npm run build + + - name: Remove client_bundle from .gitignore + run: | + sed -i '/client_bundle\|dash\|GITIGNORE/d' .gitignore + + - name: When on nightly build, append .dev0+date to version + # Run for nightly build only + if: | + (github.event_name == 'schedule' && github.ref == 'refs/heads/main') + run: | + DATE=$(date +%Y%m%d) + VERSION=$(poetry version -s) + poetry version "${VERSION}.dev0+${DATE}" + + - name: Set package_version output + id: set_package_version + run: | + VERSION=$(poetry version -s) + echo "package_version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create Python and NPM package via poetry + run: | + poetry build + + - name: Install testing dependencies + run: | + poetry install --with dev --no-ansi --no-interaction + poetry run playwright install chromium + poetry run playwright install-deps chromium + + # Install Xvfb and Mesa for headless OpenGL rendering (needed for VTK and similar libraries) + - name: Install Xvfb and Mesa + run: | + sudo apt-get update + sudo apt-get install -y xvfb mesa-utils libgl1 + + # Start Xvfb on display :99 to provide a virtual X server for graphical tests + - name: Start Xvfb + run: | + Xvfb :99 -screen 0 1920x1080x24 & + echo "DISPLAY=:99" >> $GITHUB_ENV + + - name: Run tests + continue-on-error: true + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + run: | + set -o pipefail + mkdir -p tests/artifacts/smoke/screenshots + mkdir -p tests/artifacts/smoke/reports + poetry run pytest tests/e2e/smoke -m "not saf" \ + --html=tests/artifacts/smoke/reports/smoke-tests.html \ + --self-contained-html \ + --junitxml=tests/artifacts/smoke/reports/smoke-junit.xml \ + --verbose | tee tests/artifacts/smoke/reports/smoke-result.log + + # Check for test failures in the log, so the workflow only fails if tests actually fail, + # not just because of X server errors at shutdown + + - name: Check for test failures and errors + id: check_fail + env: + MISSING_XML_MODE: error + DETECT_ERRORS: "true" + run: | + bash .github/scripts/check_junit_results.sh \ + "smoke:tests/artifacts/smoke/reports/smoke-junit.xml" + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-logs + path: tests/artifacts/smoke + + # --- SAF smoke tests --- + # - name: Install SAF CLI + # run: pip install ansys-saf-cli + + # - name: Clone saf-visor-poc + # uses: actions/checkout@v4 + # with: + # repository: ansys-internal/saf-visor-poc + # token: ${{ secrets.PYANSYS_CI_BOT_TOKEN }} + # path: saf-visor-poc + + # - name: Install saf-visor-poc dependencies + # env: + # POETRY_HTTP_BASIC_SOLUTIONS_PRIVATE_PYPI_USERNAME: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_USERNAME }} + # POETRY_HTTP_BASIC_SOLUTIONS_PRIVATE_PYPI_PASSWORD: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_PASSWORD }} + # working-directory: saf-visor-poc + # run: | + # poetry run saf install . + + # - name: Run SAF smoke tests + # continue-on-error: true + # env: + # DISPLAY: ":99" + # VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" + # run: | + # set -o pipefail + # mkdir -p tests/artifacts/saf-smoke/reports + # poetry run pytest tests/e2e/smoke/test_smoke_saf_visor.py \ + # --saf-project-dir "${{ github.workspace }}/saf-visor-poc" \ + # --html=tests/artifacts/saf-smoke/reports/saf-smoke-tests.html \ + # --self-contained-html \ + # --junitxml=tests/artifacts/saf-smoke/reports/saf-smoke-junit.xml \ + # --verbose \ + # | tee tests/artifacts/saf-smoke/reports/saf-smoke-result.log + + # - name: Check SAF smoke test results + # if: always() + # id: check_saf + # env: + # MISSING_XML_MODE: error + # DETECT_ERRORS: "true" + # run: | + # bash .github/scripts/check_junit_results.sh \ + # "saf-smoke:tests/artifacts/saf-smoke/reports/saf-smoke-junit.xml" + + # - name: Upload SAF smoke test logs + # if: always() + # uses: actions/upload-artifact@v4 + # with: + # name: saf-smoke-logs + # path: tests/artifacts/saf-smoke + + - name: Upload Python package + uses: actions/upload-artifact@v4 + with: + name: visor + path: dist/*.whl + + - name: Release to Azure Solutions PyPI + if : (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) + uses: ansys-internal/solution-applications-actions/release-to-private-pypi@v18 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + twine-repository-url: https://pkgs.dev.azure.com/ansys-solutions/_packaging/ansys-solutions/pypi/upload/ + twine-username: "TOKEN" + twine-password: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_UPLOAD_TOKEN }} + + - name: Release to Azure PyAnsys PyPI + if: | + (github.event_name == 'schedule' && github.ref == 'refs/heads/main') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) + uses: ansys-internal/solution-applications-actions/release-to-private-pypi@v18 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + twine-repository-url: https://pkgs.dev.azure.com/pyansys/_packaging/ansys-solutions/pypi/upload/ + twine-username: "TOKEN" + twine-password: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_ADMIN_TOKEN }} + + outputs: + package_version: ${{ steps.set_package_version.outputs.package_version }} + + sbom: + name: Generate and Publish SBOM + needs: build + if: | + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) + uses: ansys-internal/saf-github-workflow-templates/.github/workflows/_sbom.yml@v4 + with: + package_type: poetry + sbom-name: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && format('viewer-visor/{0}', github.ref_name) || 'viewer-visor' }} + publish: true + secrets: inherit + + docker: + needs: build + if: | + always() && + needs.build.result == 'success' && + ((github.event_name == 'schedule' && github.ref == 'refs/heads/main') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set image name and tags + id: set_image_vars + run: | + VERSION="${{ needs.build.outputs.package_version }}" + SAFE_VERSION="${VERSION//+/_}" + if [[ "$SAFE_VERSION" =~ _beta\.dev0_[0-9]{8}$ ]]; then + IMAGE_NAME="visor_dev" + elif [[ "$SAFE_VERSION" =~ _beta$ ]]; then + IMAGE_NAME="visor_beta" + elif [[ "$SAFE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + IMAGE_NAME="visor" + else + IMAGE_NAME="visor_dev" + fi + echo "image_name=$IMAGE_NAME" >> $GITHUB_OUTPUT + echo "image_tags=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:latest,ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$SAFE_VERSION" >> $GITHUB_OUTPUT + + - name: Build and push release Docker image + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + uses: docker/build-push-action@v5 + with: + context: . + file: deploy/docker/release/Dockerfile + push: true + tags: ${{ steps.set_image_vars.outputs.image_tags }} + build-args: | + PACKAGE_VERSION=${{ needs.build.outputs.package_version }} + secrets: | + PYANSYS_PYPI_PRIVATE_PAT=${{ secrets.PYANSYS_PYPI_PRIVATE_PAT }} + + - name: Build and push dev Docker image + if: | + (github.event_name == 'schedule' && github.ref == 'refs/heads/main') + uses: docker/build-push-action@v5 + with: + context: . + file: deploy/docker/dev/Dockerfile + push: true + tags: ${{ steps.set_image_vars.outputs.image_tags }} \ No newline at end of file diff --git a/.github/workflows/check-linked-issue.yml b/.github/workflows/check-linked-issue.yml new file mode 100644 index 00000000..d5343b31 --- /dev/null +++ b/.github/workflows/check-linked-issue.yml @@ -0,0 +1,26 @@ +name: Check Linked Issue + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +jobs: + check-issue-link: + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: read + steps: + - name: Check if PR is linked to an issue + uses: actions/github-script@v7 + with: + script: | + const prBody = context.payload.pull_request.body; + const issueRegex = /(close[sd]?|fix(e[sd])?|resolve[sd]?|address(e[sd])?)\s+#\d+/i; + + if (!prBody || !issueRegex.test(prBody)) { + core.setFailed("โŒ Pull request must be linked to an issue."); + shouldFail = true; + } else { + console.log("โœ… Pull request is correctly linked to an issue."); + } \ No newline at end of file diff --git a/.github/workflows/check-pr-docs-build.yml b/.github/workflows/check-pr-docs-build.yml new file mode 100644 index 00000000..18b78c82 --- /dev/null +++ b/.github/workflows/check-pr-docs-build.yml @@ -0,0 +1,53 @@ +name: Docs Build (PR) +# The documentation is built and deployed in the nightly-docs.yml workflow. +# This workflow builds the documentation for pull requests to the main branch +# to catch any issues building the documentation before merging. + + +on: + pull_request: + branches: + - main + + +env: + # Uncomment the following line to enable custom CNAME for documentation + # DOCUMENTATION_CNAME: 'visor.staging.local' + MAIN_PYTHON_VERSION: '3.11' + + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + +jobs: + docs_build: + name: Build docs + runs-on: ubuntu-latest + steps: + # Install Xvfb and Mesa for headless OpenGL rendering (needed for VTK and similar libraries) + - name: Install Xvfb and Mesa + run: | + sudo apt-get update + sudo apt-get install -y xvfb mesa-utils libgl1 + + # Start Xvfb on display :99 to provide a virtual X server for graphical tests + - name: Start Xvfb + run: | + Xvfb :99 -screen 0 1920x1080x24 & + echo "DISPLAY=:99" >> $GITHUB_ENV + + # Run tests with DISPLAY=:99 (for Xvfb) and VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 + # VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 ensures VTK uses offscreen rendering to avoid X errors + - name: Run Ansys documentation building action + uses: ansys/actions/doc-build@v10 + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + check-links: false + sphinxopts: '-j auto' + optional-dependencies-name: "" + group-dependencies-name: "doc" diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml new file mode 100644 index 00000000..d8d7cefe --- /dev/null +++ b/.github/workflows/check-pr-title.yml @@ -0,0 +1,20 @@ +name: Check PR title + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + validate_pr_title: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/generate_legal_notice.yml b/.github/workflows/generate_legal_notice.yml new file mode 100644 index 00000000..eed1a553 --- /dev/null +++ b/.github/workflows/generate_legal_notice.yml @@ -0,0 +1,164 @@ +# Collect Python + npm third-party license data, merge into one PDF, and publish as a +# downloadable workflow artifact. +# +# Manual run: Actions โ†’ "Generate Legal Notice for Visor" โ†’ Run workflow. +# Artifact: <PROJECT_NAME>_LEGAL_NOTICE[_<version>].pdf (download from the completed run). + +name: Generate Legal Notice for Visor + +on: + workflow_dispatch: + inputs: + project_name: + description: Project name used in the legal notice PDF filename + required: false + type: string + default: Visor + version: + description: Optional label appended to the artifact name (e.g. release version) + required: false + type: string + reference: + description: Branch, tag, or SHA to analyze (defaults to the commit that triggered the run) + required: false + type: string + workflow_call: + inputs: + project_name: + description: Project name used in the legal notice PDF filename + required: false + type: string + default: Visor + version: + description: Optional label appended to the artifact name + required: false + type: string + reference: + description: Branch, tag, or SHA to analyze + required: false + type: string + +jobs: + collect-py-licenses: + name: Collect Python dependency licenses + uses: ansys-internal/saf-github-workflow-templates/.github/workflows/_collect_python_dependencies.yml@v5 + with: + repository: ${{ github.repository }} + reference: ${{ inputs.reference || github.sha }} + pyproject-parent-directory: . + direct-dependencies-only: false + template-ref: v5 + secrets: inherit + + collect-npm-visor-client: + name: Collect visor-client npm licenses + uses: ansys-internal/saf-github-workflow-templates/.github/workflows/_collect_npm_dependencies.yml@v5 + with: + repository: ${{ github.repository }} + reference: ${{ inputs.reference || github.sha }} + package-json-parent-directory: src/ansys/visor/visor-client + packages-to-filter-out: '@tybys/wasm-util' + node-version: 20 + template-ref: v5 + secrets: inherit + + # SGWT artifact name is still licenses_information_npm_visor โ€” save before dash overwrites it. + stage-npm-visor-client: + name: Stage visor-client npm licenses + needs: collect-npm-visor-client + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v6 + with: + name: licenses_information_npm_visor + path: npm-visor-client + - uses: actions/upload-artifact@v4 + with: + name: npm-licenses-staging-visor-client + path: npm-visor-client + if-no-files-found: error + + collect-npm-dash: + name: Collect dash npm licenses + needs: stage-npm-visor-client + uses: ansys-internal/saf-github-workflow-templates/.github/workflows/_collect_npm_dependencies.yml@v5 + with: + repository: ${{ github.repository }} + reference: ${{ inputs.reference || github.sha }} + package-json-parent-directory: src/ansys/visor/dash + node-version: 20 + template-ref: v5 + secrets: inherit + + stage-npm-dash: + name: Stage dash npm licenses + needs: collect-npm-dash + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v6 + with: + name: licenses_information_npm_visor + path: npm-dash + - uses: actions/upload-artifact@v4 + with: + name: npm-licenses-staging-visor-dash + path: npm-dash + if-no-files-found: error + + merge-npm-licenses: + name: Merge npm licenses for PDF + needs: + - stage-npm-visor-client + - stage-npm-dash + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v6 + with: + path: npm-staged + pattern: npm-licenses-staging-* + merge-multiple: true + + - name: Merge npm_dependencies.json and license directories + shell: bash + run: | + set -euo pipefail + mkdir -p merged/licenses_information merged/copyright_information + python3 <<'PY' + import json + from pathlib import Path + + merged: dict = {} + for path in sorted(Path("npm-staged").rglob("npm_dependencies.json")): + with path.open(encoding="utf-8") as fh: + data = json.load(fh) + if isinstance(data, dict): + merged.update(data) + else: + raise TypeError(f"Unexpected JSON in {path}: {type(data)}") + + out = Path("merged/npm_dependencies.json") + out.write_text(json.dumps(merged, indent=2, sort_keys=True), encoding="utf-8") + print(f"Merged {len(merged)} npm packages into {out}") + PY + for dir_name in licenses_information copyright_information; do + find npm-staged -type d -name "$dir_name" | while read -r src_dir; do + cp -a "$src_dir/." "merged/$dir_name/" + done + done + + - uses: actions/upload-artifact@v4 + with: + name: licenses_information_npm_visor + path: merged + if-no-files-found: error + + generate-pdf: + name: Generate legal notice PDF + needs: + - collect-py-licenses + - merge-npm-licenses + uses: ansys-internal/saf-github-workflow-templates/.github/workflows/_generate_legal_notice_pdf.yml@v5 + with: + title: ${{ inputs.project_name }} + template-ref: v5 + secrets: inherit diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml new file mode 100644 index 00000000..58343abc --- /dev/null +++ b/.github/workflows/label.yml @@ -0,0 +1,105 @@ +name: Labeler + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + - labeled + +permissions: + contents: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + labeler: + name: Set labels + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + + steps: + - uses: actions/checkout@v4 + + # Apply labels based on .github/labeler.yml (if present) + - name: Label based on changed files + uses: actions/labeler@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + sync-labels: true + + # Label documentation branches + - name: Label documentation + if: | + startsWith(github.event.pull_request.head.ref, 'doc') || + startsWith(github.event.pull_request.head.ref, 'docs') + run: gh pr edit "$PR_NUMBER" --add-label "documentation" --repo "$REPO" + + # Label maintenance branches + - name: Label maintenance + if: | + startsWith(github.event.pull_request.head.ref, 'maint') || + startsWith(github.event.pull_request.head.ref, 'ci') + run: gh pr edit "$PR_NUMBER" --add-label "maintenance" --repo "$REPO" + + # Label feature branches + - name: Label enhancement + if: startsWith(github.event.pull_request.head.ref, 'feat') + run: gh pr edit "$PR_NUMBER" --add-label "enhancement" --repo "$REPO" + + # Label bug fixes + - name: Label bug + if: | + startsWith(github.event.pull_request.head.ref, 'fix') || + startsWith(github.event.pull_request.head.ref, 'patch') + run: gh pr edit "$PR_NUMBER" --add-label "bug" --repo "$REPO" + + commenter: + name: Suggest labels + needs: labeler + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - name: Suggest labels + if: toJSON(github.event.pull_request.labels.*.name) == '{}' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + gh pr comment "$PR_NUMBER" \ + --repo "$REPOSITORY" \ + --body "Please add one of these labels so the changelog can be generated: **bug**, **enhancement**, **documentation**, **maintenance**, **dependencies**, **breaking**, **test**, or **miscellaneous**." + + changelog-fragment: + name: "Create changelog fragment" + needs: [labeler] + permissions: + contents: write # Required to create changelog fragment + pull-requests: write # Required to create changelog fragment + runs-on: ubuntu-latest + steps: + - uses: ansys/actions/doc-changelog@84b6533ae76f5dde69a1df26c18682160a8121cf # v10.3.5 + with: + token: ${{ secrets.PYANSYS_CI_BOT_TOKEN }} + use-pull-request-title: true + use-default-towncrier-config: true + bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} + bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} \ No newline at end of file diff --git a/.github/workflows/nightly-docs.yml b/.github/workflows/nightly-docs.yml new file mode 100644 index 00000000..a51b815c --- /dev/null +++ b/.github/workflows/nightly-docs.yml @@ -0,0 +1,75 @@ +name: Nightly Documentation Build + +on: + schedule: # UTC at 23:00 = 7pm EDT + - cron: '0 23 * * *' + workflow_dispatch: + push: + branches: ["main"] + tags: [ 'v*' ] + +env: + # Uncomment the following line to enable custom CNAME for documentation + # DOCUMENTATION_CNAME: 'visor.staging.local' + MAIN_PYTHON_VERSION: '3.11' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + docs_build: + name: Build docs + runs-on: ubuntu-latest + steps: + # Install Xvfb and Mesa for headless OpenGL rendering (needed for VTK and similar libraries) + - name: Install Xvfb and Mesa + run: | + sudo apt-get update + sudo apt-get install -y xvfb mesa-utils libgl1 + + # Start Xvfb on display :99 to provide a virtual X server for graphical tests + - name: Start Xvfb + run: | + Xvfb :99 -screen 0 1920x1080x24 & + echo "DISPLAY=:99" >> $GITHUB_ENV + + # Run tests with DISPLAY=:99 (for Xvfb) and VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 + # VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 ensures VTK uses offscreen rendering to avoid X errors + - name: Run Ansys documentation building action + uses: ansys/actions/doc-build@v10 + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + check-links: false + sphinxopts: '-j auto' + optional-dependencies-name: "" + group-dependencies-name: "doc" + + docs_upload: + needs: docs_build + runs-on: ubuntu-latest + steps: + - name: Deploy development documentation + uses: ansys/actions/doc-deploy-dev@v10 + with: + # Uncomment the following line to enable custom CNAME for documentation + # cname: ${{ env.DOCUMENTATION_CNAME }} + token: ${{ secrets.GITHUB_TOKEN }} + bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} + bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} + + docs_upload_stable: + if: startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/release/') + needs: docs_build + runs-on: ubuntu-latest + steps: + - name: Deploy stable documentation + uses: ansys/actions/doc-deploy-stable@v10 + with: + token: ${{ secrets.GITHUB_TOKEN }} + bot-user: ${{ secrets.PYANSYS_CI_BOT_USERNAME }} + bot-email: ${{ secrets.PYANSYS_CI_BOT_EMAIL }} \ No newline at end of file diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml new file mode 100644 index 00000000..68e4fb16 --- /dev/null +++ b/.github/workflows/nightly-tests.yml @@ -0,0 +1,123 @@ +name: Nightly Tests + +on: + schedule: # UTC at 22:00 = 6pm EDT + - cron: '0 23 * * *' + workflow_dispatch: + +env: + MAIN_PYTHON_VERSION: '3.11' + + +jobs: + nightly_test: + name: Testing + if: | + (github.event_name == 'schedule' && github.ref == 'refs/heads/main') || + github.event_name == 'workflow_dispatch' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest ] + python-version: ['3.11', '3.12', '3.13', '3.14'] + + steps: + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout repository (for CI scripts) + uses: actions/checkout@v4 + with: + sparse-checkout: .github/scripts + + - name: Pull nightly dev Docker image + run: docker pull ghcr.io/${{ github.repository_owner }}/visor_dev:latest + + - name: Ensure artifacts directory exists + run: mkdir -p artifacts + + - name: Run container and execute coverage tests + id: coverage_tests + continue-on-error: true + run: | + docker run --rm \ + -v ${{ github.workspace }}/artifacts:/results \ + ghcr.io/${{ github.repository_owner }}/visor_dev:latest \ + pytest --cov=src/ansys/visor/viewer tests\ + --color=yes \ + --cov-report=term \ + --cov-report=html:/results/htmlcov \ + --cov-report=xml:/results/coverage.xml + + - name: Run container and execute regression tests + id: regression_tests + if: steps.coverage_tests.outcome == 'success' + continue-on-error: true + run: | + mkdir -p artifacts/regressions/screenshots + mkdir -p artifacts/regressions/reports + docker run --rm \ + -v ${{ github.workspace }}/artifacts:/tests/artifacts \ + ghcr.io/${{ github.repository_owner }}/visor_dev:latest \ + bash -c "pytest tests/e2e/regressions --html=/tests/artifacts/regressions/reports/regressions-tests.html --self-contained-html --junitxml=/tests/artifacts/regressions/reports/regressions-junit.xml --verbose | tee /tests/artifacts/regressions/reports/regressions-result.log" + + + - name: Check for test failures and errors + if: always() + id: check_fail + env: + MISSING_XML_MODE: error + DETECT_ERRORS: "true" + COVERAGE_OUTCOME: ${{ steps.coverage_tests.outcome }} + REGRESSION_OUTCOME: ${{ steps.regression_tests.outcome }} + run: | + # If the coverage step failed, the regression step was skipped and + # no regression JUnit XML exists. Report the coverage failure + # directly instead of misreporting it as a missing regression result. + if [ "$COVERAGE_OUTCOME" != "success" ]; then + echo "::error::Coverage tests step did not succeed (outcome=$COVERAGE_OUTCOME). Regression tests were skipped." + { + echo "exit_code=2" + echo "failed_suites=" + echo "errored_suites=coverage" + } >> "$GITHUB_OUTPUT" + exit 0 + fi + bash .github/scripts/check_junit_results.sh \ + "regression:artifacts/regressions/reports/regressions-junit.xml" + + - name: Notify Teams on failure or error + if: always() && steps.check_fail.outputs.exit_code != '0' + uses: mikesprague/teams-incoming-webhook-action@v1 + with: + github-token: ${{ github.token }} + webhook-url: ${{ secrets.MS_TEAMS_WEBHOOK_URL }} + title: >- + ${{ steps.check_fail.outputs.exit_code == '1' && 'โŒ Regression tests FAILED' || + steps.check_fail.outputs.exit_code == '2' && 'โš ๏ธ Regression tests ERRORED' || + 'โŒโš ๏ธ Regression tests FAILED and ERRORED' }} + color: >- + ${{ steps.check_fail.outputs.exit_code == '2' && 'warning' || 'failure' }} + show-commit-message: true + message: | + **Workflow:** nightly-tests.yml + **Branch:** ${{ github.head_ref || github.ref_name }} + **Python:** ${{ matrix.python-version }} + **Failed suites:** ${{ steps.check_fail.outputs.failed_suites || 'none' }} + **Errored suites:** ${{ steps.check_fail.outputs.errored_suites || 'none' }} + **View run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + ${{ steps.check_fail.outputs.exit_code == '2' && '๐Ÿ’ก *Errors indicate infrastructure issues (Docker crash, server timeout). This may not be a code problem โ€” re-running the workflow may help.*' || '' }} + # This action sends an Adaptive Card to the configured Teams channel + # via the Incoming Webhook URL. [4](https://mikesprague.github.io/teams-incoming-webhook-action/) + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ github.job }}-py${{ matrix.python-version }} + path: artifacts \ No newline at end of file diff --git a/.github/workflows/publish-architecture.yml b/.github/workflows/publish-architecture.yml new file mode 100644 index 00000000..1706fbaf --- /dev/null +++ b/.github/workflows/publish-architecture.yml @@ -0,0 +1,25 @@ +name: Publish Architecture + +on: + push: + branches: + - main + paths: + - 'developer-docs/architecture/**' +jobs: + publish-structurizr: + runs-on: ['ansys-internal-arc-dind-rss'] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Publish Structurizr Workspace + uses: ansys-internal/actions/structurizr-cli-publish@v6 + with: + workspace-id: '2' + api-key: '77e2981f-e7d6-47da-931f-61948abb3249' + api-secret: ${{ secrets.VISOR_STRUCTURIZR_ONPREM_API_SECRET }} + workspace-json: './developer_docs/architecture/workspace.json' + acr-endpoint: ${{ vars.IT_AKS_ACR_ENDPOINT }} + acr-username: ${{ vars.CI_TEMPLATES_ACR_USER }} + acr-token: ${{ secrets.CI_TEMPLATES_ACR_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 00000000..1951ee6e --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,261 @@ +# This workflow will install Python dependencies, run Python tests with the versions of Python +# that are defined in python-version + + +name: Visor Python Tests + +on: + workflow_dispatch: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + contents: read + +env: + POETRY_VERSION: '2.3.2' + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + python-version: ["3.11", '3.12', '3.13', '3.14'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - --version ${{ env.POETRY_VERSION }} + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Create venv + run: | + cd "$GITHUB_WORKSPACE" + python -m venv .venv + + - name: Install dependencies + run: | + poetry install --no-interaction --no-ansi --with setup + + - name: Run setup step + run: | + cd "$GITHUB_WORKSPACE" + poetry run visor-setup + + - name: Build web module + run: | + cd "$GITHUB_WORKSPACE/src/ansys/visor/visor-client" + npm -i install + npm run build + + - name: Other install dependencies + run: | + poetry install --no-interaction --no-ansi + + - name: Install dash npm dependencies + working-directory: src/ansys/visor/dash + run: npm install + + - name: Build dash npm package + working-directory: src/ansys/visor/dash + run: | + poetry run npm run build + + - name: Run frontend unit tests (Jest) + continue-on-error: true + working-directory: src/ansys/visor/visor-client + env: + JEST_JUNIT_OUTPUT_DIR: ../../../../tests/artifacts/unit + JEST_JUNIT_OUTPUT_NAME: frontend-junit.xml + run: | + set -o pipefail + mkdir -p ../../../../tests/artifacts/unit + npx jest --ci --reporters=default --reporters=jest-junit --verbose 2>&1 | tee ../../../../tests/artifacts/unit/frontend-result.log + +# --- Install testing dependencies --- + - name: Install Playwright and browsers + run: | + poetry install --with dev --no-ansi --no-interaction + poetry run playwright install chromium + poetry run playwright install-deps chromium + + # Install Xvfb and Mesa for headless OpenGL rendering (needed for VTK and similar libraries) + - name: Install Xvfb and Mesa + run: | + sudo apt-get update + sudo apt-get install -y xvfb mesa-utils libgl1 + + # Start Xvfb on display :99 to provide a virtual X server for graphical tests + - name: Start Xvfb + run: | + Xvfb :99 -screen 0 1920x1080x24 & + echo "DISPLAY=:99" >> $GITHUB_ENV + + - name: Collect test items + run: | + poetry run pytest tests --collect-only --verbose + + # Run tests with DISPLAY=:99 (for Xvfb) and VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 + # VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 ensures VTK uses offscreen rendering to avoid X errors + - name: Run Python unit tests + continue-on-error: true + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + run: | + set -o pipefail # Ensure the step fails if pytest fails, even with tee + mkdir -p tests/artifacts/unit + poetry run pytest ./tests/unit --html=tests/artifacts/unit/unit-tests.html --self-contained-html --junitxml=tests/artifacts/unit/unit-junit.xml --verbose | tee tests/artifacts/unit/unit-result.log + + - name: Run Python integration tests + continue-on-error: true + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + run: | + set -o pipefail # Ensure the step fails if pytest fails, even with tee + mkdir -p tests/artifacts/integration + poetry run pytest ./tests/integration --html=tests/artifacts/integration/integration-tests.html --self-contained-html --junitxml=tests/artifacts/integration/integration-junit.xml --verbose | tee tests/artifacts/integration/integration-result.log + + + - name: Run Smoke tests + continue-on-error: true + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + run: | + set -o pipefail + mkdir -p tests/artifacts/smoke/screenshots + mkdir -p tests/artifacts/smoke/reports + poetry run pytest tests/e2e/smoke -m "not saf" --html=tests/artifacts/smoke/reports/smoke-tests.html --self-contained-html --junitxml=tests/artifacts/smoke/reports/smoke-junit.xml --verbose | tee tests/artifacts/smoke/reports/smoke-result.log + + # SAF-related steps temporarily disabled + # # saf-visor-poc currently only supports Python 3.11 and 3.12 + # - name: Install SAF CLI + # if: matrix.python-version == '3.11' || matrix.python-version == '3.12' + # run: pip install ansys-saf-cli + + # - name: Clone saf-visor-poc + # if: matrix.python-version == '3.11' || matrix.python-version == '3.12' + # uses: actions/checkout@v4 + # with: + # repository: ansys-internal/saf-visor-poc + # token: ${{ secrets.PYANSYS_CI_BOT_TOKEN }} + # path: saf-visor-poc + + # - name: Install saf-visor-poc dependencies + # if: matrix.python-version == '3.11' || matrix.python-version == '3.12' + # env: + # POETRY_HTTP_BASIC_SOLUTIONS_PRIVATE_PYPI_USERNAME: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_USERNAME }} + # POETRY_HTTP_BASIC_SOLUTIONS_PRIVATE_PYPI_PASSWORD: ${{ secrets.SOLUTIONS_PRIVATE_PYPI_PASSWORD }} + # working-directory: saf-visor-poc + # run: poetry run saf install . + + # - name: Run SAF smoke tests + # if: matrix.python-version == '3.11' || matrix.python-version == '3.12' + # continue-on-error: true + # env: + # DISPLAY: ":99" + # VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" + # run: | + # set -o pipefail + # mkdir -p tests/artifacts/saf-smoke/reports + # poetry run pytest tests/e2e/smoke/test_smoke_saf_visor.py \ + # --saf-project-dir "${{ github.workspace }}/saf-visor-poc" \ + # --html=tests/artifacts/saf-smoke/reports/saf-smoke-tests.html \ + # --self-contained-html \ + # --junitxml=tests/artifacts/saf-smoke/reports/saf-smoke-junit.xml \ + # --verbose \ + # | tee tests/artifacts/saf-smoke/reports/saf-smoke-result.log + + - name: Run Regression tests + continue-on-error: true + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + run: | + set -o pipefail + mkdir -p tests/artifacts/regressions/screenshots + mkdir -p tests/artifacts/regressions/reports + poetry run pytest tests/e2e/regressions \ + --html=tests/artifacts/regressions/reports/regressions-tests.html \ + --self-contained-html \ + --junitxml=tests/artifacts/regressions/reports/regressions-junit.xml \ + --verbose \ + | tee tests/artifacts/regressions/reports/regressions-result.log + + - name: Run notebook tests (nbval) + continue-on-error: true + env: + DISPLAY: ":99" # Use the virtual X server started by Xvfb + VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN: "1" # Force VTK to use offscreen rendering + run: | + mkdir -p tests/artifacts/notebook + NOTEBOOK_PATH="tests/notebooks" + + if [ -d "$NOTEBOOK_PATH" ]; then + echo "Running nbval on $NOTEBOOK_PATH ..." + poetry run pytest --nbval "$NOTEBOOK_PATH" --html=tests/artifacts/notebook/nbval-tests.html --self-contained-html --junitxml=tests/artifacts/notebook/notebook-junit.xml --verbose | tee tests/artifacts/notebook/nbval-result.log || true + + else + echo "No '$NOTEBOOK_PATH' directory found; skipping nbval run." | tee tests/artifacts/notebook/nbval-result.log + fi + + # Check for test failures in the log, so the workflow only fails if tests actually fail, + # not just because of X server errors at shutdown + + - name: Check for test failures and errors + if: always() + id: check_results + env: + MISSING_XML_MODE: error + DETECT_ERRORS: "true" + run: | + bash .github/scripts/check_junit_results.sh \ + "unit:tests/artifacts/unit/unit-junit.xml" \ + "frontend-unit:tests/artifacts/unit/frontend-junit.xml" \ + "integration:tests/artifacts/integration/integration-junit.xml" \ + "smoke:tests/artifacts/smoke/reports/smoke-junit.xml" \ + "regression:tests/artifacts/regressions/reports/regressions-junit.xml" \ + "notebook:tests/artifacts/notebook/notebook-junit.xml" + + # saf-smoke tests commented out temporarily. have to + # comment out this whole block or else CI/CD fails. + # - name: Check for test failures and errors + # if: always() + # id: check_results + # env: + # MISSING_XML_MODE: error + # DETECT_ERRORS: "true" + # run: | + # saf-smoke XML only exists on Python versions that ran the SAF steps + # SAF_SMOKE_ARG="" + # if [ -f "tests/artifacts/saf-smoke/reports/saf-smoke-junit.xml" ]; then + # SAF_SMOKE_ARG="saf-smoke:tests/artifacts/saf-smoke/reports/saf-smoke-junit.xml" + # fi + # bash .github/scripts/check_junit_results.sh \ + # "unit:tests/artifacts/unit/unit-junit.xml" \ + # "frontend-unit:tests/artifacts/unit/frontend-junit.xml" \ + # "integration:tests/artifacts/integration/integration-junit.xml" \ + # "smoke:tests/artifacts/smoke/reports/smoke-junit.xml" \ + # $SAF_SMOKE_ARG \ + # "regression:tests/artifacts/regressions/reports/regressions-junit.xml" \ + # "notebook:tests/artifacts/notebook/notebook-junit.xml" + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-logs-${{ github.job }}-py${{ matrix.python-version }} + path: tests/artifacts \ No newline at end of file diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml new file mode 100644 index 00000000..2360f9c4 --- /dev/null +++ b/.github/workflows/security_scan.yml @@ -0,0 +1,24 @@ +name: Security Scan + +on: + workflow_dispatch: + pull_request: + branches: + - main + - release/* + push: + tags: + - "v*" + branches: + - main + - release/* + +jobs: + security_scan: + if: ${{ github.actor != 'dependabot[bot]' }} + uses: ansys-internal/ci-templates/.github/workflows/security-scan-mend.yml@v12 + with: + package_type: 'poetry+npm' + needs: 'sca,sast,sbom' + poetry-version: '2.1.3' + secrets: inherit \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f51b0295 --- /dev/null +++ b/.gitignore @@ -0,0 +1,215 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +# lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +src/ansys/visor/viewer/client_bundle + +# Dash component build outputs (auto-generated by `npm run build`) +src/ansys/visor/dash/.Rbuildignore +src/ansys/visor/dash/DESCRIPTION +src/ansys/visor/dash/NAMESPACE +src/ansys/visor/dash/Project.toml +src/ansys/visor/dash/deps/ +src/ansys/visor/dash/inst/ +src/ansys/visor/dash/man/ +src/ansys/visor/dash/node_modules/ +src/ansys/visor/dash/R/ +src/ansys/visor/dash/src/jl/ +src/ansys/visor/dash/src/Visordash.jl +src/ansys/visor/dash/visordash/_imports_.py +src/ansys/visor/dash/visordash/Visordash.py +src/ansys/visor/dash/visordash/metadata.json +src/ansys/visor/dash/visordash/package-info.json +src/ansys/visor/dash/visordash/visordash.min.js +src/ansys/visor/dash/visordash/visordash.min.js.map +src/ansys/visor/dash/visordash/visordash.min.js.LICENSE.txt +__GITIGNORE_* + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Local config +.visor + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +reports/ +tests/artifacts/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# SAF solutions +saf-visor-poc/ + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +.venvdash +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + + +# Node +node_modules/ + +# Generated files +*.vtu +src/ansys/visor/visor-client/modules +src/ansys/visor/visor-client/tsconfig.app.tsbuildinfo +src/ansys/visor/visor-client/tsconfig.node.tsbuildinfo + +# Log files from running Visor +src/logs/ + +# Build artifacts from sphinx docs +doc/_build/ +doc/source/_autosummary +doc/source/_static/versions.json +doc/source/examples +doc/source/http_api_reference/openapi.json +doc/source/sg_execution_times.rst + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..a902b11d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,96 @@ +repos: + - repo: https://github.com/adamchainz/blacken-docs + rev: 1.20.0 + hooks: + - id: blacken-docs + additional_dependencies: [black==23.12.1] + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.2 + hooks: + - id: codespell + args: + - --ignore-words + - doc/styles/Vocab/ANSYS/accept.txt + exclude: | + (?x)^( + .*\.vtp$| + .*package-lock\.json$| + poetry\.lock$| + doc/styles/Vocab/ANSYS/accept\.txt + )$ + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + - id: check-yaml + - id: trailing-whitespace + exclude: ^.*\.(ipynb|svg)$ + + # - repo: https://github.com/ansys/pre-commit-hooks + # rev: v0.7.2 + # hooks: + # - id: add-license-headers + # files: '^(src|examples|tests)/.*\.(py|proto)$' + # args: + # - --start_year=2024 + + # Validate GitHub workflow files + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.37.2 + hooks: + - id: check-github-workflows + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.16 + hooks: + - id: ruff + args: [--fix] + + - repo: local + hooks: + # -- visor-client (TypeScript / React / Vite) -- + - id: visor-client-eslint + name: visor-client eslint + entry: npm --prefix src/ansys/visor/visor-client run lint + language: system + pass_filenames: false + files: ^src/ansys/visor/visor-client/.*\.(js|ts|tsx)$ + + - id: visor-client-prettier + name: visor-client prettier + entry: npm --prefix src/ansys/visor/visor-client run format:check + language: system + pass_filenames: false + files: ^src/ansys/visor/visor-client/.*\.(js|ts|tsx|css|json)$ + + # -- dash (plain JS / React / Webpack) -- + - id: dash-eslint + name: dash eslint + entry: npm --prefix src/ansys/visor/dash run lint + language: system + pass_filenames: false + files: ^src/ansys/visor/dash/src/.*\.(js|jsx)$ + + - id: dash-prettier + name: dash prettier + entry: npm --prefix src/ansys/visor/dash run format:check + language: system + pass_filenames: false + files: ^src/ansys/visor/dash/src/.*\.(js|jsx|css|json)$ + +# Exclude generated / vendored / build artifacts +exclude: | + (?x)^( + node_modules/.*| + .*dist/.*| + .*build/.*| + .*\.wasm$| + .*\.min\.js$| + src/ansys/visor/dash/.*/deps/.*| + src/ansys/visor/dash/.*/inst/deps/.*| + tests/files/.*| + examples/assets/.* + )$ \ No newline at end of file diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..2a3b3356 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,12 @@ +# This is the list of Visor's significant contributors. +# +# This file does not necessarily list everyone who has contributed code. +# +# For contributions made under a Corporate CLA, the organization is +# added to this file. +# +# If you have contributed to the repository and want to be added to this file, +# submit a request. +# +# +ANSYS, Inc. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..6eba046f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +This project uses [towncrier](https://towncrier.readthedocs.io/) and the changes for the upcoming release can be found in <https://github.com/ansys-internal/visor/tree/main/doc/changelog.d/>. + +<!-- towncrier release notes start --> \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5aab4f5d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contribute + +Overall guidance on contributing to a PyAnsys library appears in the +[Contributing] topic in the *PyAnsys developer's guide*. Ensure that you +are thoroughly familiar with this guide before attempting to contribute to +Visor. + +The following contribution information is specific to Visor. + +Additional information can be found in the +[Visor Contributing](https://vigilant-lamp-162kw9z.pages.github.io/version/dev/contributing/index.html) +documentation. + +[Contributing]: https://dev.docs.pyansys.com/how-to/contributing.html + +<!-- Begin content specific to your library here. --> + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..86e10d21 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,13 @@ +# Contributors + +## Project Lead + +* [Marina Galvagni](https://github.com/margalva) + +## Individual Contributors + +* [BasavaRaju Akula](https://github.com/ansBAkula) +* [Ben Brooks](https://github.com/ansbbrooks) +* [Laura Kasian](https://github.com/LKasianAnsys) +* [Sergio Fernรกndez](https://github.com/AnSergioFern) +* [Vamshi Sama](https://github.com/vsama98) diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..46119fa1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +************************************************************* + +Copyright (c) 2025 Synopsys, Inc. and ANSYS, Inc. +All Rights Reserved. + +Restricted Rights Legend + +Use, duplication, or disclosure of this software +and its documentation by the U.S. Government is +subject to restrictions as set forth in subdivision +[(b)(3)(ii)] of the Rights in Technical Data and Computer +Software clause at 52.227-7013 +************************************************************* diff --git a/README.md b/README.md deleted file mode 100644 index dd401c3b..00000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# visor \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..7fa6ac28 --- /dev/null +++ b/README.rst @@ -0,0 +1,102 @@ + +Visor +===== + +|pyansys| + +.. |pyansys| image:: https://img.shields.io/badge/Py-Ansys-ffc107.svg?labelColor=black&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABDklEQVQ4jWNgoDfg5mD8vE7q/3bpVyskbW0sMRUwofHD7Dh5OBkZGBgW7/3W2tZpa2tLQEOyOzeEsfumlK2tbVpaGj4N6jIs1lpsDAwMJ278sveMY2BgCA0NFRISwqkhyQ1q/Nyd3zg4OBgYGNjZ2ePi4rB5loGBhZnhxTLJ/9ulv26Q4uVk1NXV/f///////69du4Zdg78lx//t0v+3S88rFISInD59GqIH2esIJ8G9O2/XVwhjzpw5EAam1xkkBJn/bJX+v1365hxxuCAfH9+3b9/+////48cPuNehNsS7cDEzMTAwMMzb+Q2u4dOnT2vWrMHu9ZtzxP9vl/69RVpCkBlZ3N7enoDXBwEAAA+YYitOilMVAAAAAElFTkSuQmCC + :target: https://docs.pyansys.com/ + :alt: PyAnsys + + +.. contents:: + :local: + +Demo +---- + +You can view a `demo <https://visor.dev.ansysapis.com/index.html>`_ of +Visor to see it in action. + + +Overview +-------- + +Visor is a Python-based 3D visualization web component. + +You can use Visor to integrate with the following tools: + +* `SAF (Solution Application Framework) <https://upgraded-carnival-wn6lkym.pages.github.io>`_ + to provide 3D visualization capabilities. +* `Ansys Dynamic Reporting <https://nexusdemo.ensight.com/docs/html/Nexus.html>`_ + to support dynamic reporting workflows. +* `PyAnsys Visualization Interface Tool <https://visualization-interface.tools.docs.pyansys.com/>`_ + to connect PyAnsys libraries to different plotting backends. + +You can use Visor for 3D visualization on desktop and on premises. Visor customizes existing +rendering frameworks to meet functional and nonfunctional requirements, including integrability, +scalability, performance, and usability for solution apps. + +You run Visor with VTK (Visual Toolkit) rendering and Trame to support a client-server architecture and Python integration +workflows. You target VTK WASM with WebGL because WebGPU support is not yet available. + + +Installation +------------ + +To set up prerequisites and install Visor, see +`Getting started <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/getting_started/index.html>`_ +in the Visor documentation. + + +Quick start +----------- + +For a basic usage example, see +`Quick start <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/getting_started/quick_start.html>`_ +in the Visor documentation. + + +Development +----------- + +To set up your development environment, see the `Contributing.md <Contributing.md>`_ file in the +Visor repository. + + +Usage +----- + +You can use Visor through the following entry points. Links are to the relevant information in he +Visor documentation. + +* **Standalone Python** + + * Launch a desktop Visor app with the Python package ``ansys.visor.viewer`` to visualize files. + * See `Visor Python API <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/user_guide/launching_theia/theia_python_api.html>`_ + for API usage examples. + * See `API reference <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/python_api_reference.html>`_ + for details about the Python API. + +* **Visor HTTP service** + + * Manage multiple Visor instances through the Visor HTTP API. + * Integrate with `PIM (Product Instance Management) <https://github.com/ansys/ansys-api-platform-instancemanagement>`_ + to manage Visor instances in SAF apps. + * See `Visor HTTP service <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/user_guide/launching_theia/theia_service.html>`_ + for usage examples. + * See `HTTP API reference <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/http_api_reference/index.html>`_ + for API details. + +* **Dash frontend app** + + * Connect to a Visor instance from a Dash app with the Visor Dash component in + ``ansys.visor.dash.dash_visor_viewer``. + * See `Visor Dash component <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/user_guide/launching_theia/theia_dash_component.html>`_ + for details. + +For advanced usage and configuration options, see the following Visor documentation: + +`User guide <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/user_guide/index.html>`_ and +`Examples <https://vigilant-lamp-162kw9z.pages.github.io/version/dev/examples/index.html>`_ in the +Visor sections. diff --git a/deploy/docker/README.md b/deploy/docker/README.md new file mode 100644 index 00000000..41753445 --- /dev/null +++ b/deploy/docker/README.md @@ -0,0 +1,184 @@ +# Running Visor in a Docker Container + +The Visor project contains Docker configurations for both development and release environments. + +For each environment, there is a Dockerfile and docker-compose.yml to enable containerizing the FastAPI service, +including its frontend and backend. The Docker configurations are located in the `deploy/docker/dev` directory for development +and `deploy/docker/release` for production. + +The containers can be used for local development, debugging, and CI (e.g. running unit tests in GitHub workflows). + +Dockerfile +* Installs system and Python dependencies (via poetry) +* Runs visor-setup +* Builds the frontend assets +* Starts the FastAPI app with uvicorn + +docker-compose.yml +* Maps the ports (53211, 8081) for frontend/backend access +* Sets the PYTHONPATH environment variable +* Can be extended for additional configuration + +## Prerequisites +* [Podman](https://podman.io/) or [Docker ](https://www.docker.com/) installed + + +## Building and running the docker container +We can build and run the docker container using straight docker commands, or we can use docker compose. + +Note that on desktops, Ansys is recommending using podman, which offers analogous commands to docker. The docker commands are shown below, but they should be interchangeable with podman commands. (Note that podman also accepts the Dockerfile and docker-compose.yml files as defaults) + + +### Option 1: Build and run with Docker (or Podman) +#### i. Build the image +``` +docker build -t visor-image -f docker/dev/Dockerfile . +``` +#### ii. Run the container +``` +# on windows: +docker run --name visor-service -p 53211:53211 -p 8081:8081 -v %cd%:/workspace visor-image +# on linux: +docker run --name visor-service -p 53211:53211 -p 8081:8081 -v $(pwd):/workspace visor-image +# Access FastAPI at http://localhost:53211 +``` + +The above command mounts the current directory into the container at `/workspace`, +which is where the FastAPI service expects to find the project files. +You can adjust the volume mount as needed for your project structure. +See the section on [Volume Mounts](#volume-mounts) below for more details. + +#### iii. Stop and remove the container +``` +docker container stop visor-service +docker container rm visor-service +docker image rm visor-image +``` + +### Option 2: Use Docker Compose +Docker compose uses the `docker-compose.yml` file to build and run the container, +which simplifies the process by managing the configuration and dependencies in a +single file. + +The docker-compose.yml file is located in the root of the Visor project directory. + +#### i. Build the image and run the container + +To build and run the container using docker compose, run: +``` +docker compose -f deploy/docker/dev/docker-compose.yml up +# uses existing image if available +# To build a new image first, use `docker compose up --build` +``` + +#### ii. Stop and and remove the container (and optionally the image) + +To stop the container, run: +``` +docker compose down +# this kills the container but keeps the image +# to remove the image as well, use: +# docker compose -f deploy/docker/dev/docker-compose.yml down --rmi +``` + +### Access the FastAPI service +Once the container is running, you can access the FastAPI service at: http://localhost:53211 + +When making calls to the FastAPI service, the `/start` and `/update` APIs +take a `file_path` parameter. Note that if a relative path is provided, it is relative +to the `/app` directory, which contains only the data that was copied when the image was built. + +If you need to specify a file, e.g. `example_file.vtm`, that is included in the +mounted volume (e.g. a file in your local project directory), but was not included in the docker image at the time it was built, +you can specify the file path as follows: + +``` +{ + "file_path": "/workspace/example_file.vtm" +} +``` + +Similarly, if you have a custom directory mounted into the container at `/workspace`, +you can specify the file path relative to that directory. + + +### Volume Mounts + +The docker containers that are generated from the above sections provide +access to the user's local disk at runtime, by mounting a volume from the local host +into the container. +This allows the container to read and write files directly from the host filesystem. + +Using the above commands, the user's current directory (the top level directory of the +visor repository) is mounted into the container at `/workspace`. + +You can adjust the volume mount as needed for your project structure. + +#### i. Mounting a Custom Directory Using Docker Run Command +The docker run command shown in [Section ii. Run the container](#ii-run-the-container) +mounts the current directory into the container at `/workspace`. +You can adjust the volume mount as needed for your project structure. + +For example, if you are running the docker container from the `visor` project root +and would like to mount a specific data directory for testing or development, +you can specify a path like this: + +``` +docker run --name visor-service -p 53211:53211 -p 8081:8081 -v C:\ANSYSDev\NoBackup\example-data:/workspace visor-image +``` +#### ii. Using Docker Compose +The above command can also be specified in the `docker-compose.yml` file. +On Windows machines, due to the way the file paths are handled, we need to handle +the volume mounts a bit differently than on Linx or MacOS. + + +##### On Linux/MacOS: +To mount a specific data directory, you can modify the `volumes` section like this: + +```yaml +volumes: + - /path/to/example-data:/workspace +``` + +#### On Windows: +To mount a specific data directory, you can modify the `volumes` section as follows. +Note this is using the WSL path format, which is required for Docker on Windows. +The absolute path should be specified in the WSL format, which is typically `/mnt/c/` for the C: drive. + + +```yaml + +services: + backend: + # other configurations... + volumes: + - local_data_volume:/workspace + + +volumes: + local_data_volume: + driver: local + driver_opts: + type: none + o: bind + # On windows, set path using the WSL path format + # e.g. for a Windows path like C:\ANSYSDev\NoBackup\example-data: + device: /mnt/c/ANSYSDev/NoBackup/example-data +``` +This code for adding the volume mount is included as a comment in the `docker-compose.yml` file, +so you can uncomment and modify it as needed. + +### Notes: +Exec into a running container using bash: +``` +docker exec -it visor-service /bin/bash +``` + +Commands to find and remove all containers and/or images: +``` +docker container list -a | grep -v NAMES | awk '{print $NF}' | xargs podman container rm +docker image list -a | grep -v "IMAGE ID" | awk '{print $3}' | xargs podman image rm +``` +Note: All commands are interchangeable with docker if preferred. + + diff --git a/deploy/docker/dev/Dockerfile b/deploy/docker/dev/Dockerfile new file mode 100644 index 00000000..cae97cf9 --- /dev/null +++ b/deploy/docker/dev/Dockerfile @@ -0,0 +1,78 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies required for: +# - Running Python backend (libx11-6, libxrender1 for VTK and headless rendering) +# - Running frontend build tools (nodejs, npm for React/TypeScript build) +# - Enabling headless GUI support (xvfb for tests or rendering that require a display) +# - System and network utilities (procps, psutils, net-tools, iproute2 for debugging & monitoring) +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libx11-6 \ + libxrender1 \ + xvfb \ + nodejs \ + npm \ + procps \ + psutils \ + net-tools \ + net-tools \ + iproute2 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Set up Xvfb for headless GUI support. +# This creates a shell script that starts a virtual X server (Xvfb) on display :99, +# enabling applications that require a display (e.g., VTK rendering, browser tests) to run in the container. +RUN echo 'Xvfb :99 -screen 0 1024x768x24 & export DISPLAY=:99' > /etc/profile.d/xvfb.sh \ + && chmod +x /etc/profile.d/xvfb.sh \ + && echo 'source /etc/profile.d/xvfb.sh' >> /root/.bashrc + +# Install poetry +RUN pip install --no-cache-dir poetry + +# Copy backend files +COPY pyproject.toml poetry.lock ./ +COPY src ./src + +# Copy frontend (visor-client) code +COPY src/ansys/visor/visor-client ./src/ansys/visor/visor-client + +# Copy README.rst +COPY README.rst ./ + +# Copy dev-only directories +COPY tests ./tests +COPY doc ./doc +COPY examples ./examples + +# Install backend dependencies with dev (for testing including Playwright) and setup groups +RUN POETRY_VIRTUALENVS_CREATE=false poetry install --with dev,setup --no-interaction --no-ansi + +# Run setup script +RUN visor-setup + +# Build frontend +WORKDIR /app/src/ansys/visor/visor-client +RUN npm install \ + && chmod +x ./node_modules/.bin/tsc ./node_modules/.bin/vite \ + && npm run build + +# Build dash component +WORKDIR /app/src/ansys/visor/dash +RUN npm install \ + && npm run build + +WORKDIR /app + +RUN poetry run playwright install chromium +RUN poetry run playwright install-deps chromium + +EXPOSE 53211 +EXPOSE 8081 +ENV PYTHONPATH=/app/src + +ENTRYPOINT ["/bin/bash", "-c", "rm -f /tmp/.X99-lock; Xvfb :99 -screen 0 1024x768x24 & export DISPLAY=:99 && exec \"$@\"", "--"] + +CMD ["uvicorn", "ansys.visor.viewer.api.server:app", "--host", "0.0.0.0", "--port", "53211"] diff --git a/deploy/docker/dev/docker-compose.yml b/deploy/docker/dev/docker-compose.yml new file mode 100644 index 00000000..f1fc0182 --- /dev/null +++ b/deploy/docker/dev/docker-compose.yml @@ -0,0 +1,46 @@ + +services: + backend: + container_name: visor-service-dev + build: + context: ../../.. + dockerfile: deploy/docker/dev/Dockerfile + ports: + # Ports for visor server + - "53211:53211" + # Ports for visor trame server + - "8081:8081" + - "8082:8082" + - "8083:8083" + - "8084:8084" + - "8085:8085" + - "8086:8086" + # Ports for visor dash component + - "8050:8050" + - "8051:8051" + - "8052:8052" + environment: + - PYTHONPATH=/app/src + volumes: + - ../../..:/workspace + # Alternative if using the local_data_volume: + # - local_data_volume:/workspace + + +# To customize the local directory being mounted in the container: +# 1. Modify the `device` option in the `volumes` section below. +# Note: The path should be the absolute path to the directory +# 2. Uncomment the volume definition below. +# 3. Remove the line "- .:/workspace" under the "volumes" section of the +# `backend` service above and uncomment the alternative, "- local_data_volume:/workspace" + + +# volumes: +# local_data_volume: +# driver: local +# driver_opts: +# type: none +# o: bind +# # On windows, set path using the WSL path format +# # e.g. for a Windows path like C:\ANSYSDev\NoBackup\example-data: +# device: /mnt/c/ANSYSDev/NoBackup/example-data diff --git a/deploy/docker/fuji/Dockerfile b/deploy/docker/fuji/Dockerfile new file mode 100644 index 00000000..f5e9053d --- /dev/null +++ b/deploy/docker/fuji/Dockerfile @@ -0,0 +1,54 @@ +# --- Release Stage --- +FROM python:3.11-slim AS release + +WORKDIR /app + +# Install runtime dependencies +# - Running Python backend (libx11-6, libxrender1 for VTK and headless rendering) +# - Enabling headless GUI support (xvfb for tests or rendering that require a display) +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libx11-6 \ + libxrender1 \ + xvfb \ + && rm -rf /var/lib/apt/lists/* + +# Install Python runtime dependency +RUN pip install --no-cache-dir uvicorn + +# Set up Xvfb for headless GUI support. +RUN echo 'Xvfb :99 -screen 0 1024x768x24 & export DISPLAY=:99' > /etc/profile.d/xvfb.sh \ + && chmod +x /etc/profile.d/xvfb.sh \ + && echo 'source /etc/profile.d/xvfb.sh' >> /root/.bashrc + +# Create a non-root user and set permissions +RUN useradd -m visoruser +RUN mkdir -p /app/logs && chown visoruser /app/logs +RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix + +# Copy the example assets +COPY examples /app/examples +COPY tests /app/tests + +# Install the wheel using BuildKit secret +RUN --mount=type=secret,id=pypi_pat \ + pip install --no-cache-dir ansys-visor-viewer \ + --extra-index-url https://__token__:$(cat /run/secrets/pypi_pat)@pkgs.dev.azure.com/ansys-solutions/_packaging/ansys-solutions/pypi/simple/ + +# TODO: remove this once buggy trame_vtklocal downloading behaviour is resolved +# (currently re-downloads this upon each startup even if the files exist) +RUN chown -R visoruser /usr/local/lib/python3.11/site-packages/trame_vtklocal/module + +# Set the user to run the application +USER visoruser + +EXPOSE 53211 +EXPOSE 8081 +ENV PYTHONPATH=/app/src + +ENTRYPOINT ["/bin/bash", "-c", "rm -f /tmp/.X99-lock; Xvfb :99 -screen 0 1024x768x24 & export DISPLAY=:99 && exec \"$@\"", "--"] + +CMD ["python", "examples/python/simple_example.py"] + + + diff --git a/deploy/docker/fuji/docker-compose.yml b/deploy/docker/fuji/docker-compose.yml new file mode 100644 index 00000000..69c98ca3 --- /dev/null +++ b/deploy/docker/fuji/docker-compose.yml @@ -0,0 +1,40 @@ + +services: + backend: + container_name: visor-fuji + build: + context: ../../.. + dockerfile: deploy/docker/fuji/Dockerfile + secrets: + - pypi_pat + ports: + - "53211:53211" + - "8081:8081" + environment: + - PYTHONPATH=/app/src + volumes: + - ../../..:/workspace + # Alternative if using the local_data_volume: + # - local_data_volume:/workspace + +secrets: + pypi_pat: + environment: SOLUTIONS_PYPI_PRIVATE_PAT + +# To customize the local directory being mounted in the container: +# 1. Modify the `device` option in the `volumes` section below. +# Note: The path should be the absolute path to the directory +# 2. Uncomment the volume definition below. +# 3. Remove the line "- .:/workspace" under the "volumes" section of the +# `backend` service above and uncomment the alternative, "- local_data_volume:/workspace" + + +# volumes: +# local_data_volume: +# driver: local +# driver_opts: +# type: none +# o: bind +# # On windows, set path using the WSL path format +# # e.g. for a Windows path like C:\ANSYSDev\NoBackup\example-data: +# device: /mnt/c/ANSYSDev/NoBackup/example-data diff --git a/deploy/docker/release/Dockerfile b/deploy/docker/release/Dockerfile new file mode 100644 index 00000000..10cc359d --- /dev/null +++ b/deploy/docker/release/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.11-slim AS release + +ARG PACKAGE_VERSION + +RUN echo "Building ansys-visor-viewer version: $PACKAGE_VERSION" + +WORKDIR /app + +# Install runtime dependencies +# - Running Python backend (libx11-6, libxrender1 for VTK and headless rendering) +# - Enabling headless GUI support (xvfb for tests or rendering that require a display) +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libx11-6 \ + libxrender1 \ + xvfb \ + && rm -rf /var/lib/apt/lists/* + +# Install Python runtime dependency +RUN pip install --no-cache-dir uvicorn + +# Set up Xvfb for headless GUI support. +RUN echo 'Xvfb :99 -screen 0 1024x768x24 & export DISPLAY=:99' > /etc/profile.d/xvfb.sh \ + && chmod +x /etc/profile.d/xvfb.sh \ + && echo 'source /etc/profile.d/xvfb.sh' >> /root/.bashrc + +# Create a non-root user and set permissions +RUN useradd -m visoruser +RUN mkdir -p /app/logs && chown visoruser /app/logs +RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix + +# Secure pip install using BuildKit secret +RUN --mount=type=secret,id=SOLUTIONS_PYPI_PRIVATE_PAT \ + pip install --no-cache-dir ansys-visor-viewer==${PACKAGE_VERSION} \ + --extra-index-url "https://__token__:$(cat /run/secrets/SOLUTIONS_PYPI_PRIVATE_PAT)@pkgs.dev.azure.com/ansys-solutions/_packaging/ansys-solutions/pypi/simple/" + +# Set the user to run the application +USER visoruser + +EXPOSE 53211 +EXPOSE 8081 +ENV PYTHONPATH=/app/src + +ENTRYPOINT ["/bin/bash", "-c", "rm -f /tmp/.X99-lock; Xvfb :99 -screen 0 1024x768x24 & export DISPLAY=:99 && exec \"$@\"", "--"] + +CMD ["uvicorn", "ansys.visor.viewer.api.server:app", "--host", "0.0.0.0", "--port", "53211"] + + + diff --git a/deploy/docker/release/docker-compose.yml b/deploy/docker/release/docker-compose.yml new file mode 100644 index 00000000..d1960c0f --- /dev/null +++ b/deploy/docker/release/docker-compose.yml @@ -0,0 +1,54 @@ + +services: + backend: + container_name: visor-service + build: + context: ../../.. + dockerfile: deploy/docker/release/Dockerfile + secrets: + - SOLUTIONS_PYPI_PRIVATE_PAT + args: + PACKAGE_VERSION: 1.0.3 + ports: + # Ports for visor server + - "53211:53211" + # Ports for visor trame server + - "8081:8081" + - "8082:8082" + - "8083:8083" + - "8084:8084" + - "8085:8085" + - "8086:8086" + # Ports for visor dash component + - "8050:8050" + - "8051:8051" + - "8052:8052" + environment: + - PYTHONPATH=/app/src + volumes: + - ../../..:/workspace + # Alternative if using the local_data_volume: + # - local_data_volume:/workspace + +secrets: + SOLUTIONS_PYPI_PRIVATE_PAT: + environment: SOLUTIONS_PYPI_PRIVATE_PAT + + +# To customize the local directory being mounted in the container: +# 1. Modify the `device` option in the `volumes` section below. +# Note: The path should be the absolute path to the directory +# 2. Uncomment the volume definition below. +# 3. Remove the line "- .:/workspace" under the "volumes" section of the +# `backend` service above and uncomment the alternative, "- local_data_volume:/workspace" + + +# volumes: +# local_data_volume: +# driver: local +# driver_opts: +# type: none +# o: bind +# # On windows, set path using the WSL path format +# # e.g. for a Windows path like C:\ANSYSDev\NoBackup\example-data: +# device: /mnt/c/ANSYSDev/NoBackup/example-data diff --git a/doc/.vale.ini b/doc/.vale.ini new file mode 100644 index 00000000..b702418a --- /dev/null +++ b/doc/.vale.ini @@ -0,0 +1,35 @@ +# Core settings +# ============= + +# Location of our `styles` +StylesPath = "styles" + +# The options are `suggestion`, `warning`, or `error` (defaults to โ€œwarningโ€). +MinAlertLevel = warning + +# By default, `code` and `tt` are ignored. +IgnoredScopes = code, tt + +# By default, `script`, `style`, `pre`, and `figure` are ignored. +SkippedScopes = script, style, pre, figure + +# WordTemplate specifies what Vale will consider to be an individual word. +WordTemplate = \b(?:%s)\b + +# List of Packages to be used for our guidelines +Packages = Google + +# Define the Ansys vocabulary +Vocab = ANSYS + +[*.{md,rst}] + +# Apply the following styles +BasedOnStyles = Vale, Google + +# Inline roles are ignored +TokenIgnores = (:.*:`.*`)|(<.*>) + +# Removing Google-specific rule - Not applicable under some circumstances +Google.WordList = NO +Google.Colons = NO \ No newline at end of file diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 00000000..5c3a8f31 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,32 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -j auto +SPHINXBUILD = sphinx-build +SOURCEDIR = source +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + + +# customized clean due to examples gallery +clean: + rm -rf build + rm -rf $(SOURCEDIR)/_autosummary + rm -rf $(SOURCEDIR)/examples + +# Create PDF +pdf: + @$(SPHINXBUILD) -M latex "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + cd $(BUILDDIR)/latex && latexmk -r latexmkrc -pdf *.tex -interaction=nonstopmode || true + (test -f $(BUILDDIR)/latex/*.pdf && echo pdf exists) || exit 1 diff --git a/doc/changelog.d/1.maintenance.md b/doc/changelog.d/1.maintenance.md new file mode 100644 index 00000000..a29c0488 --- /dev/null +++ b/doc/changelog.d/1.maintenance.md @@ -0,0 +1 @@ +Copy VISOR project from internal repo diff --git a/doc/changelog.d/1192.test.md b/doc/changelog.d/1192.test.md new file mode 100644 index 00000000..9a84562a --- /dev/null +++ b/doc/changelog.d/1192.test.md @@ -0,0 +1 @@ +Test/sfernand/save_load_state diff --git a/doc/changelog.d/1195.documentation.md b/doc/changelog.d/1195.documentation.md new file mode 100644 index 00000000..1556f5b0 --- /dev/null +++ b/doc/changelog.d/1195.documentation.md @@ -0,0 +1 @@ +Remote rendering ADR diff --git a/doc/changelog.d/1196.maintenance.md b/doc/changelog.d/1196.maintenance.md new file mode 100644 index 00000000..6fa5ad7a --- /dev/null +++ b/doc/changelog.d/1196.maintenance.md @@ -0,0 +1 @@ +Changelog creation diff --git a/doc/changelog.d/1199.test.md b/doc/changelog.d/1199.test.md new file mode 100644 index 00000000..cb9e3608 --- /dev/null +++ b/doc/changelog.d/1199.test.md @@ -0,0 +1 @@ +Test/sfernand/add jest to cicd diff --git a/doc/changelog.d/1200.miscellaneous.md b/doc/changelog.d/1200.miscellaneous.md new file mode 100644 index 00000000..35f3d041 --- /dev/null +++ b/doc/changelog.d/1200.miscellaneous.md @@ -0,0 +1 @@ +Remote rendering 1.1 - split scene from pipeline diff --git a/doc/changelog.d/1204.miscellaneous.md b/doc/changelog.d/1204.miscellaneous.md new file mode 100644 index 00000000..f9e37dc2 --- /dev/null +++ b/doc/changelog.d/1204.miscellaneous.md @@ -0,0 +1 @@ +Remote rendering 1.2 - add iRenderer and LocalRenderer to backend diff --git a/doc/changelog.d/1216.maintenance.md b/doc/changelog.d/1216.maintenance.md new file mode 100644 index 00000000..eca85aa4 --- /dev/null +++ b/doc/changelog.d/1216.maintenance.md @@ -0,0 +1 @@ +Update changelog version diff --git a/doc/changelog.d/1217.miscellaneous.md b/doc/changelog.d/1217.miscellaneous.md new file mode 100644 index 00000000..bbcbe62e --- /dev/null +++ b/doc/changelog.d/1217.miscellaneous.md @@ -0,0 +1 @@ +Remote rendering 1.2b - make TheiaSceneBase abstract and create TheiaLocalScene subclass diff --git a/doc/changelog.d/1218.added.md b/doc/changelog.d/1218.added.md new file mode 100644 index 00000000..687be596 --- /dev/null +++ b/doc/changelog.d/1218.added.md @@ -0,0 +1 @@ +Add ability to scan for free port in initialize/ diff --git a/doc/changelog.d/1234.added.md b/doc/changelog.d/1234.added.md new file mode 100644 index 00000000..dfa14af0 --- /dev/null +++ b/doc/changelog.d/1234.added.md @@ -0,0 +1 @@ +Remote rendering 1.3 - add RenderingMode enum to dispatch LOCAL (VTK.wasm) mode diff --git a/doc/changelog.d/1236.maintenance.md b/doc/changelog.d/1236.maintenance.md new file mode 100644 index 00000000..01f226c9 --- /dev/null +++ b/doc/changelog.d/1236.maintenance.md @@ -0,0 +1 @@ +Ruff N-rule compliance first pass: Python naming to snake_case diff --git a/doc/changelog.d/1237.maintenance.md b/doc/changelog.d/1237.maintenance.md new file mode 100644 index 00000000..ed2eb1c0 --- /dev/null +++ b/doc/changelog.d/1237.maintenance.md @@ -0,0 +1 @@ +Ruff N-rule compliance second pass: Test fixes diff --git a/doc/changelog.d/1238.documentation.md b/doc/changelog.d/1238.documentation.md new file mode 100644 index 00000000..429d07c3 --- /dev/null +++ b/doc/changelog.d/1238.documentation.md @@ -0,0 +1 @@ +Overall review for public release diff --git a/doc/changelog.d/1239.maintenance.md b/doc/changelog.d/1239.maintenance.md new file mode 100644 index 00000000..90604a7c --- /dev/null +++ b/doc/changelog.d/1239.maintenance.md @@ -0,0 +1 @@ +Enable Ruff N-rule and final pass for python style compliance diff --git a/doc/changelog.d/1240.maintenance.md b/doc/changelog.d/1240.maintenance.md new file mode 100644 index 00000000..4fe6c2d0 --- /dev/null +++ b/doc/changelog.d/1240.maintenance.md @@ -0,0 +1 @@ +Bump fast-uri to 3.1.5 per CVE-2026-18446 diff --git a/doc/changelog.d/1243.documentation.md b/doc/changelog.d/1243.documentation.md new file mode 100644 index 00000000..13de99a1 --- /dev/null +++ b/doc/changelog.d/1243.documentation.md @@ -0,0 +1 @@ +Overall documentation review followup items diff --git a/doc/changelog.d/1244.added.md b/doc/changelog.d/1244.added.md new file mode 100644 index 00000000..3eaed761 --- /dev/null +++ b/doc/changelog.d/1244.added.md @@ -0,0 +1 @@ +Separate bind host from public host diff --git a/doc/changelog.d/1245.test.md b/doc/changelog.d/1245.test.md new file mode 100644 index 00000000..e84bdc88 --- /dev/null +++ b/doc/changelog.d/1245.test.md @@ -0,0 +1 @@ +Test/sfernand/saf theia smoke diff --git a/doc/changelog.d/1249.added.md b/doc/changelog.d/1249.added.md new file mode 100644 index 00000000..5666a75a --- /dev/null +++ b/doc/changelog.d/1249.added.md @@ -0,0 +1 @@ +Remote rendering 2.1 - add frontend irenderer diff --git a/doc/changelog.d/1253.added.md b/doc/changelog.d/1253.added.md new file mode 100644 index 00000000..77058628 --- /dev/null +++ b/doc/changelog.d/1253.added.md @@ -0,0 +1 @@ +Remote rendering 2.2 - Use irenderer in scene graph diff --git a/doc/changelog.d/1254.documentation.md b/doc/changelog.d/1254.documentation.md new file mode 100644 index 00000000..5742f3b9 --- /dev/null +++ b/doc/changelog.d/1254.documentation.md @@ -0,0 +1 @@ +Fix two issues with contributing.md diff --git a/doc/changelog.d/1256.maintenance.md b/doc/changelog.d/1256.maintenance.md new file mode 100644 index 00000000..b3436137 --- /dev/null +++ b/doc/changelog.d/1256.maintenance.md @@ -0,0 +1 @@ +Remove arrayIndex and enable selection by spectrumName diff --git a/doc/changelog.d/1259.maintenance.md b/doc/changelog.d/1259.maintenance.md new file mode 100644 index 00000000..2be766d0 --- /dev/null +++ b/doc/changelog.d/1259.maintenance.md @@ -0,0 +1 @@ +Rename 'theia' to 'theia' in backend diff --git a/doc/changelog.d/1268.maintenance.md b/doc/changelog.d/1268.maintenance.md new file mode 100644 index 00000000..f6043aa9 --- /dev/null +++ b/doc/changelog.d/1268.maintenance.md @@ -0,0 +1 @@ +Fix dash snapshot diff --git a/doc/changelog.d/1275.maintenance.md b/doc/changelog.d/1275.maintenance.md new file mode 100644 index 00000000..9cbb6e1e --- /dev/null +++ b/doc/changelog.d/1275.maintenance.md @@ -0,0 +1 @@ +Address Pydantic and FastAPI deprecation warnings diff --git a/doc/changelog.d/1285.fixed.md b/doc/changelog.d/1285.fixed.md new file mode 100644 index 00000000..c93290b3 --- /dev/null +++ b/doc/changelog.d/1285.fixed.md @@ -0,0 +1 @@ +Cell data selection method diff --git a/doc/changelog.d/1286.maintenance.md b/doc/changelog.d/1286.maintenance.md new file mode 100644 index 00000000..4f4dae51 --- /dev/null +++ b/doc/changelog.d/1286.maintenance.md @@ -0,0 +1 @@ +Rename 'theia' to 'theia' in frontend diff --git a/doc/changelog.d/1287.maintenance.md b/doc/changelog.d/1287.maintenance.md new file mode 100644 index 00000000..537dae6b --- /dev/null +++ b/doc/changelog.d/1287.maintenance.md @@ -0,0 +1 @@ +Standardize POINT/CELL field association across client and server diff --git a/doc/changelog.d/1290.fixed.md b/doc/changelog.d/1290.fixed.md new file mode 100644 index 00000000..8aeaf5b4 --- /dev/null +++ b/doc/changelog.d/1290.fixed.md @@ -0,0 +1 @@ +Docker nightly diff --git a/doc/changelog.d/1291.maintenance.md b/doc/changelog.d/1291.maintenance.md new file mode 100644 index 00000000..eac28c4a --- /dev/null +++ b/doc/changelog.d/1291.maintenance.md @@ -0,0 +1 @@ +Rename 'theia' to 'visor' in documentation diff --git a/doc/changelog.d/1293.maintenance.md b/doc/changelog.d/1293.maintenance.md new file mode 100644 index 00000000..91342592 --- /dev/null +++ b/doc/changelog.d/1293.maintenance.md @@ -0,0 +1 @@ +Bump package version to 1.0.5_beta diff --git a/doc/changelog.d/1296.miscellaneous.md b/doc/changelog.d/1296.miscellaneous.md new file mode 100644 index 00000000..54581b8f --- /dev/null +++ b/doc/changelog.d/1296.miscellaneous.md @@ -0,0 +1 @@ +[Remote rendering 3.0a] introduce the renderer annotation and build scene details from it diff --git a/doc/changelog.d/1297.documentation.md b/doc/changelog.d/1297.documentation.md new file mode 100644 index 00000000..ba98c801 --- /dev/null +++ b/doc/changelog.d/1297.documentation.md @@ -0,0 +1 @@ +Minor edits and fixed broken images (fixed PR) diff --git a/doc/changelog.d/1298.miscellaneous.md b/doc/changelog.d/1298.miscellaneous.md new file mode 100644 index 00000000..4a40da37 --- /dev/null +++ b/doc/changelog.d/1298.miscellaneous.md @@ -0,0 +1 @@ +[Remote rendering 3.0b] bind client to the new renderer annotation diff --git a/doc/changelog.d/1299.miscellaneous.md b/doc/changelog.d/1299.miscellaneous.md new file mode 100644 index 00000000..69ac2de0 --- /dev/null +++ b/doc/changelog.d/1299.miscellaneous.md @@ -0,0 +1 @@ +[Remote rendering 3.0c] remove legacy scene details fields and bump schema version to 2 diff --git a/doc/changelog.d/1300.miscellaneous.md b/doc/changelog.d/1300.miscellaneous.md new file mode 100644 index 00000000..4e82635e --- /dev/null +++ b/doc/changelog.d/1300.miscellaneous.md @@ -0,0 +1 @@ +[Remote rendering 3.0d] type the variable field association diff --git a/doc/changelog.d/1304.fixed.md b/doc/changelog.d/1304.fixed.md new file mode 100644 index 00000000..9b9dda76 --- /dev/null +++ b/doc/changelog.d/1304.fixed.md @@ -0,0 +1 @@ +Add pointerEvents: none to invisible wrapper div diff --git a/doc/changelog.d/changelog_template.jinja b/doc/changelog.d/changelog_template.jinja new file mode 100644 index 00000000..3ca0146b --- /dev/null +++ b/doc/changelog.d/changelog_template.jinja @@ -0,0 +1,22 @@ +{% if sections[""] %} + +.. tab-set:: + +{%+ for category, val in definitions.items() if category in sections[""] %} + + .. tab-item:: {{ definitions[category]['name'] }} + + .. list-table:: + :header-rows: 0 + :widths: auto + +{% for text, values in sections[""][category].items() %} + * - {{ text }} + - {{ values|join(', ') }} + +{% endfor %} +{% endfor %} + +{% else %} +No significant changes. +{% endif %} diff --git a/doc/developer_docs/adrs/01-visor-tenets.md b/doc/developer_docs/adrs/01-visor-tenets.md new file mode 100644 index 00000000..f103b258 --- /dev/null +++ b/doc/developer_docs/adrs/01-visor-tenets.md @@ -0,0 +1,44 @@ +# VISOR - (Visual Interactive Simulation Object Renderer) Solutions Applications 3D Visualization Components Tenets + +## Decision + +VISOR (Visual Interactive Simulation Object Renderer, from here on "VISOR") is a framework providing Solutions Applications the necessary visualization components in order to be able to support the diversity of requirements for 3D visualization needs of Solution Applications while being able to follow the following tenets in terms terms non-functional requirements: + * Seamless integration and architecture compatibility with SAF + * Seamless integration with Ansys Dynamic Reporting + * Ease of use and integratability delivered through PyAnsys initiatives (pyansys-visualization-tools) + * Scalability in terms of supporting more complicated 3D models, multiple users + * Deploymentability without sacrificing functionality, performance and maintainability requirements for desktop, on-prem and cloud architectures. The deployment processes should adhere to KPIs such as Deployment Frequency, Lead Time for Changes, Change Failure Rate, Deployment Duration, Automated Test Coverage, Resource Utilization, User Impact, Deployment Success Rate. In order to achieve the previous, containerizing and deploying the solution is part of our process and the KPIs towards that include Image Build Time, Image Size, Image PUll Time, Image Vulnerabilities, Image Layers, Image Age, Image Reuse, Image Compatibility, Resource Utilization, Compliance, Automated Test Coverage. + * Performance in terms of web component KPIs as as well as service side KPIs. + * Maintainability and alignment with best practises and development processes of CASEBU STCs. One of the processes followed is to have our documentation based on the relevant CASEBU ADRs in terms of architecture and developer documentation and examples of using this component. + +## Context + +VISOR is a new STC which is aiming to provide Solution Applications a set of 3D visualization components which can support all the different aspects of functional and non-functional requirements. As this is targeting solutions applications instead of a specific product and is created as part of the Enablement Platform AIEP development framework there are specific tenets that make sense to be agreed upon now that its being started so that they can remain and evolve along with the project. + + +## Options + +1. โœ”๏ธ Framework that follows an architecture design which can scale in terms of adding different frameworks and providing customized components for the different integrations and functional and non-functional requirements as they are being created +1. โŒ New 3D Viewer Rendering Components +1. โŒ Specific 3D Viewer / 3D Viewer Framework (Trame, Ceetron Hoops, AVZ) + +## Consequences + +1. โœ”๏ธ It depends on technologies provided by other internal or external teams and provides all the integration interfaces, APIs and tools targeting CASEBU STCs and products which are included in Solution Applications. The rationale for choosing this option is that: + - the functional requirements can be provided by existing and under development 3D visualization technologies + - the non-functional requirements are not provided by any existing or under development 3D visualization technology + - solutions applications have extremely diverse needs and a matrix of deployment options and there is a lot of effort that needs engineering expertise to make the integration robust, secure, scalable and easy to integrate by ACE groups. + - even though it leads to adding complexity to the components as they need to provide flexibility for rendering choices and deployment options, its the unique value that this project can provide which external technologies cannot do, as they cannot work this closely with products and ACE groups. + - it does lead to depending on different rendering technologies not necessarily owned by the same team or group and can be external companies, but its chosen due to time frame and resourcing constraints +1. โŒ New 3D Viewer Rendering Components + - It's not recommended even though there is expertise in that area which we can utilize due to time-frame and resourcing constraints + - It is also an issue of separating concerns and being able to put resources on providing components which ACE groups developing Solutions can integrate and be productive so that a continuous pain point is addressed + - Even if the same group develops new rendering technologies to address problems that cannot be solved from other groups that doesn't mean they cannot be integrated with this framework when they are read and provide an easier path for migration to new graphics technologies for Solutions Applications rather than a straight on integration. +1. โŒ Specific 3D Viewer / 3D Viewer Framework (Trame, Ceetron Hoops, AVZ) + - This is the fastest and less complicated route for first delivery but its not future proof + - It leads to vendor-locking + - None of the existing technologies can currently deliver the full list of functional and non-functional requirements so its inevitable that we are going to keep going forward and keep evolving our visualization tools to meet the growing needs of the Solutions group. + +## Advice + +Complete advice process recorded [here](https://github.com/ansys-internal/aap/discussions/43). \ No newline at end of file diff --git a/doc/developer_docs/adrs/02-visor-technology-components.md b/doc/developer_docs/adrs/02-visor-technology-components.md new file mode 100644 index 00000000..d82c0f2d --- /dev/null +++ b/doc/developer_docs/adrs/02-visor-technology-components.md @@ -0,0 +1,172 @@ +## VISOR technology components + +### Status +Approved + +### Decision +Based on the project tenets, the wide variety of projects to integrate with in our roadmap and our prioritized use cases and time frame, we are aiming at being technology agnostic on the API level for VISOR and provide ways to support different rendering and framework technologies on the backend. The technology we are aiming for our first release is going to be Trame framework targeting VTK WASM. We will continue researching other options and technologies as the project evolves. + +### Context + +VISOR is targeting to provide a 3D viewer web component and the services, tools and utilities to support the growing needs of Solutions Applications. The first release is aiming for Q4 2024/Q1 2025 based on continuous integration releases (QP2). The assessment in terms of options is done based on this time frame and the options that are not viable due to the timeframe we have can be evaluated for a different milestone in our roadmap. The Minimum Viable Product (MVP) shortlist is tracked [here](https://ansys-my.sharepoint.com.mcas.ms/:x:/r/personal/marina_galvagni_ansys_com/_layouts/15/Doc.aspx?sourcedoc=%7B1D341238-D500-4E4B-A2A6-11DC492C2CFB%7D&file=MVP_shortlist.xlsx&action=default&mobileredirect=true) and covers all functional requirements. Key requirements non-functional are the following: + +|No | Requirement | Priority | +----|-------------|-----------| +| 1 | JS/React Client | MVP | +| 2 | Python API (service side) | MVPt | +| 3 | Multiple user support | Long-term roadmap (to be decided) | +| 4 | Data streaming of updates | Long-term roadmap (to be decided)| +| 5 | Integratable with Dynamic Reporting | MVP | +| 6 | Integratable and interoperable with SAF components | MVP | +| 7 | Deployment target: Desktop | MVP | +| 8 | Deployment target: On Premise | MVP | +| 9 | Deployment target : public/private cloud | 3- Low | +| 10 | Ease of use through Python scripts | MVP | +| 11 | Contributors documentation | MVP | +| 12 | Internal User documentation | MVP | +| 13 | Interoperable with pyansys-visualization-tools | Long-term roadmap (to be decided) | +| 14 | Multiple viewers (single) per session | MVP| +| 15 | Single user per session | MVP | +| 16 | Multiple users per session | Long-term roadmap (to be decided) | +| 17 | Multiple sessions | Long-term roadmap (to be decided) | +| 18 | Performance targets for pipeline for SAF, DPF, HPS for small/medium non-complex meshes| MVP | +| 19 | Performance targets for pipeline for SAF, DPF, HPS, ADR for small/medium non-complex meshes| MVP (To be decided)| +| 20 | Performance targets for pipeline for SAF, DPF, HPS, ADR for larger/ complex meshes| Long-term roadmap (to be decided) | + +### Options + +There are several options for this project where each of them has different advantages and challenges. The research done for each option and the rationale behind each of them is explained as: + +1. [Trame](https://kitware.github.io/trame/) (VTK.js) :heavy_plus_sign: : This is a visualization framework that is already in production based on VTK.js and supported by Kitware. This is the mainline Trame framework already included in the official VTK releases. + + _Advantages_: + + * :heavy_plus_sign: _Released framework, so it is already available for integration and it is stable._ + * :heavy_plus_sign: _It has all the components we will require in order to get an estimation of delivering by the end of the year, given there is still risk in this estimation. We can get support from Kitware in terms of bugfixing for these_ + * :heavy_plus_sign: It provides server-side rendering capabilities for larger/more complicated models. + * It is an open source project so we don't transfer cost to ACE or our customers for using this technology. + * The GLTF 2.0 import/export. + * VTK based visualization frameworks including Trame are being used or their visualization of choice by ACE groups, PyAnsys and some Ansys products, which can aid integratability and ease of use. + + _Disadvantages_: + + * :heavy_minus_sign: Performance is poor on the client side for medium models (~3m elements) + * :heavy_minus_sign: It is going to be replaced by the new generation of WASM based rendering which has a different JS API and bindings on the client side + * There are other frameworks that can potentially provide better performance long term due to using more optimal graphics formats +and better architecture in terms of streaming APIs and services for on-prem and cloud deployment targets. + + _Mitigation_: + * We can have Kitware support on bugfixes and issues we have + * We can use server-side rendering for more complex or larger models + + +2. [Trame targeting VTK.WASM](https://github.com/Kitware/trame-vtklocal) :heavy_check_mark: : This is the new generation for Trame which uses VTK.WASM for rendering and is also going to target WebGPU from the VTK side. This has been in development phase and only now is transitioning to productization with expected release in November. + + _Advantages_: + + * :heavy_plus_sign: It has all the components we will require in order to get an estimation of delivering at the end of the year or beginning of Q1 2025, given there is still risk in this estimation. We can get support from Kitware in terms of bugfixing for these and the engineering effort for creating and maintaining the customized viewer is the smallest of the alternatives. + * It targets VTK.WASM on the client side which is able to provide the extra performance required for small and medium sized meshes. + * :heavy_plus_sign: It has an object manager API which is able to serialize/deserialize objects and be used for easier integration of client side and server side rendering as well as being able to provide easier interoperability on the client side with different client side frameworks and components. Its much easier to utilize this API from a JS API and React component to integrate with SAF rather than the alternatives which would be to define the API and the multiple layers of bindings. + * :heavy_plus_sign: It is going to be the main version of Trame on the next release of VTK.WASM and Trame frameworks. + * :heavy_plus_sign: It needs less work on the client side to create our own client side viewer. + * The new WebGPU releases on the VTK side will bring more performance on the client side so it will reduce the need for server side for medium or more complex models. + * It is an open source project so we don't transfer cost to ACE or our customers for using this technology + * VTK based visualization frameworks including Trame are being used or their visualization of choice by ACE groups, PyAnsys and some Ansys products, which can aid integratability and ease of use. + * + + _Disadvantages_: + + * :heavy_minus_sign: It is expected to be released in November + * It hasn't been stable so we will need more support from Kitware in order to be able to release in our timeframes and it's still going to be an issue if they don't make the stable release in November. + * There are other frameworks that can potentially provide better performance long term due to using more optimal graphics formats and better architecture in terms of streaming APIs and services for on-prem and cloud deployment targets. + + _Mitigation_: + * We can have Kitware support on bugfixes and issues we have + +3. [Omniverse](https://www.nvidia.com/en-us/omniverse/) (OpenUSD) :heavy_multiplication_x: : NVIDIA Omniverseโ„ข is a platform of APIs, SDKs, and services that enable developers to easily integrate Universal Scene Description (OpenUSD) and RTX rendering technologies into existing software tools and simulation workflows. There are multiple initiatives within Ansys that are integrating omniverse in their visualization workflows, which can be seen in this [slide deck](https://ansys-my.sharepoint.com/:p:/p/nicolas_dalmasso/EfBCg7P1hWRFmRQaFg1X8TABFuFUpk_imKN3IZsjayzD-g?e=966IyE) by Nicolas Dalmasso. This option hasn't been explored fully in terms of integrating the current version and then moving on to the next version of APIs and SDKs in terms of effort. There is a related task for more in-depth research though a [spike](https://github.com/ansys-internal/theia/issues/7). + + _Advantages_: + + * This platform has the latest rendering capabilities provided by NVIDIA + * It is in partnership with Ansys and there are a lot of initiatives which are integrating Omniverse with existing tools, e.g. EnSight + * It supports [Universal Scene Description (OpenUSD)](https://www.nvidia.com/en-us/omniverse/usd/) which is an open standard that Ansys is a collaborator + * It has APIs and SDK Kits for developing viewers which can utilize the rendering capabilities of RTX technologies. + * It will provide Streaming APIs covering the matrix of deployment targets Desktop, On-Prem and Cloud technologies + + _Disadvantages_: + + * It is now going through a large refactoring of their APIs which will lead to a much better performance and usability support and currently its in beta but not ready for release. + * It requires engineering effort greater than other alternatives for supporting computational meshes and operations to provide a first version for a viewer. + * It has a cost associated for using this platform as well as having the corresponding hardware in a desktop or on-prem configuration, which has to be agreed on for the Solution Applications and ACE if its going to be the default or a required option. + +4. AVZ :heavy_multiplication_x: : This is the current viewer for Solutions Applications targeting desktop integration. The option would be to re-engineer AVZ in order to be able to support all the current functional requirements, target the new Khronos Standard GLTF 2.0 and also create a service that is able to support the On-Prem and cloud deployment targets. + + _Advantages_: + * This is the current viewer which has experts within Ansys and could potentially deliver a next generation AVZ if the priorities were provided as such + * It is already integrated with ADR and in the desktop version it is integrated with SAF. + * It has proven that has the performance capabilities to be integraed in Ansys products (FLUENT). + * It is integrated with a lot of Ansys products already so there is less effort required in building interfaces with Ansys flagship products. + + _Disadvantages_: + * It needs engineering effort in order to be able to target the next generation of rendering technologies and standards required. + * It doesn't have an efficient service for on premise and cloud deployment targets. + * It is currently in flux in terms of roadmap, code ownership and codebase refactoring which needs to be resolved in order to be able to deliver our integration targets and ease of use. + +5. WebGX :heavy_multiplication_x: : This is the rendering framework developed by DBU which is targeting WebGPU. It is not researched in depth as it is currently in flux, but it could be a viable solution that we can re-evaluate. + + _Advantages_: + * This is a framework developed for web component visualization capabilities within Ansys and has expertise in Ansys. + * It is targeting WebGPU on the client side which has the potential to provide the performance requirements of VISOR. + + _Disadvantages_: + * There is no maintained viewer component for VISOR + * It is currently in flux in terms of roadmap + +6. [Hoops](https://docs.techsoft3d.com/hps/latest/index.html) :heavy_multiplication_x: : It is an engineering 3D visualization SDK and it is going to provide the services and integration for our deployment targets at their next generation of software. It has not been fully evaluated as it is a vendor specific offering which has its own internal graphics format and it was not prioritized as high as other ones. We can organise time for looking into it more thoroughly but it hasn't currently been done in depth. + + _Advantages:_ + * It provides scientific SDK for JS client which aids the ease of development of the viewer and covering functional requirements (haven't researched whether all are covered with the associated performance requirements). + * It is an established framework that we are using in Ansys products. + + _Disadvantages:_ + * It doesn't target an standard graphics format that we are contributing into + * It has vendor requirements for using it + * It is the next version which covers our non-functional requirements. + + +7. New viewer based on [Three.JS](https://threejs.org/) :heavy_multiplication_x:: This is the option of using a new custom Three.JS viewer with the use of visualization libraries to be researched and creating the service infrastructure and tools to support our functional and on-functional requirements. This option has not been researched yet, this is the corresponding [spike task](https://github.com/ansys-internal/theia/issues/18). + + _Advantages:_ + * Fully customized viewer for our needs + * Expertise within the company + + _Disadvantages:_ + * Needs the more engineering effort in order to be able to deliver the functional requirements and the non-functional ones in terms of time and resourcing + +### Consequences + +These [tenets](https://github.com/ansys-internal/theia/blob/tenets/adrs/01-theia-tenets.md) allow the project to change direction without transferring effort to the groups integrated with VISOR or at least with minimal effort. This means that the option decided now it doesn't have to be the only option going forward but it is going to necessitate engineering effort to add more targets or transition the project to a new visualization SDK or platform. The view of the project is to be itself a platform for visualization components for the Solutions Group and handle the engineering complexity of those components and integrating them in VISOR rather than pushing it to the Solutions Applications. This is also a matter of choosing vendor or making it possible to have different initial, running cost and maintenance cost for the different approaches as the deployment targets and the cost evaluation differs. + +The current target is to release in Q4 2024 a VISOR MVP version. The consequences per option are outlined in the following table: + +| Option | Technology | Decision | Reason | Potential future target | +|---------|-----------|-----------|--------|-------------------------| +| 1 | Trame VTK.JS | No | Performance is not future looking, the architecture of our viewer is simpler and more effective with the next generation, it is a risk in terms of making the Q4 release based on the Trame VTK.WASM delivering in November officially.| No | +| 2 | Trame VTK.WASM | Yes | Performance is more future looking, architecture and engineering effort on the viewer is the shortest of all the options and we have funding for Kitware resources to help us with delivering| Yes | +|3 | Omniverse | No | It requires further investigation and the streaming APIs that we would want to target are not released yet, it also has a cost, deployment requirements and we need to make sure that Solutions Applications are all agreeing on that| Yes | +|4| AVZ (next gen) | No| As we need to define the plan and roadmap for this next generation as it doesn't currently fulfill our requirements | Yes | +|5| WebGX | No | It needs further research | Yes| +|6| Hoops | No | It needs further research | Yes | +|7| New Three.js Viewer | No| It needs further research | No | + + + + + + + + + + + + diff --git a/doc/developer_docs/adrs/03-server-python-api.md b/doc/developer_docs/adrs/03-server-python-api.md new file mode 100644 index 00000000..22f5e550 --- /dev/null +++ b/doc/developer_docs/adrs/03-server-python-api.md @@ -0,0 +1,285 @@ +# ADR 03: VISOR Python API + +## Status +Decided + +## Context +The VISOR visualization component can be integrated into a Python application in the context of the pyAnsys initiative. A pythonic interface helps with interoperability with initiatives and interfaces like the pyansys-visualization-tools interface. + +## Decision +Provide a Python API which aids integration with Python projects from the PyAnsys initiative and leads to easier Python programming in Jupyter notebooks and Ansys labs as well as eases integration with ansys-visualization-interface from the PyAnsys initiative. This Python API is only meant to be used in a Python application environment and not as a way to integrate with SAF. + +### VISOR instance + +The VISOR instance is able to select the rendering engine, the url where the visualization will be starting for the native browser to be able to handle the visualization as well as the ability to have a standalone visualization or use this Python API for integrating with a local desktop Dash application. Each instance of VISOR is only going to work on a single url and session. + +VISOR is a server-client architecture and currently it only support a single backend which is using VTK.WASM utilizing the Trame framework. VISOR service is starting a [Trame](https://trame.readthedocs.io/en/latest/) [server](https://trame.readthedocs.io/en/latest/viewer.server.html) on the backend. The Trame server is calling [wslink](https://github.com/Kitware/wslink) to setup a websocket connection to from the host to the client. The only way to provide input is through the service side API and not through the client API. + +The VISOR instance marks the lifecycle of VISOR within this Python execution environment. There is no way to connect to this instance from a different Python environment, apart from the url which is executing the client. The client side is updating the input of VISOR as VISOR is a service that is always managed by the server side. When the VISOR instance is destroyed due to the Python process ending or getting out of scope all of the services and temporary files and folders need to be cleaned up. + +```text +visor_default_instance = Visor( + url: str | None = None, + input: str| vtkDataSet | None = None, + metadata: Metadata | None = None, + standalone: bool = True, +) + +``` + +The VISOR instance makes it possible to change the default configuration for url, logs, input_file_paths and all relevant settings through a Settings object constructed based on the +ansys.visor.viewer config.py file. +The Settings object is defined as follows: + +```text +class Settings(BaseSettings): + app_name: str = "VISOR Viewer" + default_host: str = "localhost" + default_port: int = 8081 + default_standalone: bool = True + default_client_bundle: str = Path("client_bundle") + default_log_dir: str = str(Path.cwd().joinpath("logs")) + trame_log_dir: str | None = None + +``` +The app_name is able to rename the application name of the component for the current execution. +The default_host and default_port make it possible to provide a different host and port of execution. +The default_standalone is whether the VISOR server is going to be hosting the webclient. +The default_client_bundle is where the client bundle has been deployed for the static client code +The default_log_dir provides the path to logs +The default_trame_log_dir provides the path to trame logging. + + + +#### Start + +```text +def start(self, input: vtkDataSet| str | None, timeout = 0) -> int +``` + +Starts the Trame server [start](https://trame.readthedocs.io/en/latest/viewer.server.html#trame_server.viewer.Server.start). All the relevant configurations have been provided at the time of instantiation from VISOR. There is the option to provide an input on the start function where it can be an in-memory vtk object in terms of a vktDataSet or a path to a file which at this point can only be a VTK formatted file. Due to VISOR accepting only its internal native format VISOR doesn't do any internal conversions. + +The visualization starts on a background thread instead of the current process to allow for the update() and stop() APIs to be used without any multi-threading management to happen on the user's side. + +```mermaid + sequenceDiagram + VISOR_instance->>Trame_server: start() + Trame_server-->>wslink: start +``` + +The relevant API is the following: +```text +// Start visualization application in the same thread. +// When the browser is closed. +def start( + self, + input: str | vtkDataSet | None, + metadata: Metadata | None = None, + timeout: int = 0, +) -> int +``` + + *Example usage* +```text + visualizer = Visor() + output = converter.to_data_set() + visualizer.start() + #...Thread continues execution while VISOR is visualizing the input +``` + +#### Update +The update function updates the input of the current visualization already running in the same Python process. +This clears any existing datasets from the scene, and adds the new dataset (and optionally metadata) to the scene. + +```text +def update(self, input: str|vtkDataSet, metadata: Metadata | None = None) -> int +``` + +Example: +```text +visualizer = Visor() + output = converter.to_vtk_file(a_processed_file,a_processed_file_vtk) + visualizer.start(input = a_processed_file_vtk) + another_output = converter.to_vtk_dataset(a_processed_result, vtk_dataset) + visualizer.update(vtk_dataset) + ### more code executed here +``` + +#### Add dataset +The `add_dataset` function adds a new dataset as input to current visualization already running in the same Python process. +This keeps any existing datasets in the scene, and adds the new dataset (and optionally metadata) to the scene. + +```text +def add_dataset(self, input: str|vtkDataSet, metadata: Metadata | None = None) -> int +``` + +Example: +```text +visualizer = Visor() + output = converter.to_vtk_file(a_processed_file,a_processed_file_vtk) + dataset_id1 = visualizer.start(input = a_processed_file_vtk) + another_output = converter.to_vtk_dataset(a_processed_result, vtk_dataset) + dataset_id2 = visualizer.add_dataset(vtk_dataset) + ### more code executed here +``` + +#### List datasetss +The `list_datasets` function lists all datasets currently in the scene of the current visualization already running in +the same Python process. The scene is not modified with this operation. + +```text +def list_datasets(self) -> list[int] +``` + +Example: +```text +visualizer = Visor() + output = converter.to_vtk_file(a_processed_file,a_processed_file_vtk) + visualizer.start(input = a_processed_file_vtk) + another_output = converter.to_vtk_dataset(a_processed_result, vtk_dataset) + visualizer.add_dataset(vtk_dataset) + visualizer.list_datasets() + ### more code executed here +``` + + +#### Remove dataset +The `remove_dataset` function removes a dataset from the current visualization already running in the same Python process. + + +```text +def remove_dataset(self, dataset_id: int) +``` + +Example: +```text +visualizer = Visor() + output = converter.to_vtk_file(a_processed_file,a_processed_file_vtk) + dataset_id1 = visualizer.start(input = a_processed_file_vtk) + another_output = converter.to_vtk_dataset(a_processed_result, vtk_dataset) + dataset_id2 = visualizer.add_dataset(vtk_dataset) + visualizer.remove_dataset(dataset_id2) + + ### more code executed here +``` + +#### List variables +The `list_variables` function lists all variables for a given dataset in the scene of the current visualization already running in +the same Python process. The scene is not modified with this operation. + +```text +def list_variables(self, dataset_id: int) -> list[VisorVariable] +``` + +Example: +```text +visualizer = Visor() + output = converter.to_vtk_file(a_processed_file,a_processed_file_vtk) + dataset_id1 = visualizer.start(input = a_processed_file_vtk) + dataset_id1_variables = visualizer.list_variables(dataset_id1) + ### more code executed here +``` + +#### Update variables +The `update_variables` function updates the variables for a given dataset in the scene of the current visualization already running in +the same Python process. + +**Limitation:** This feature is currently only supported for VTK datasets that are either vtkPolyData or vtkUnstructuredGrid. +VISOR also supports vtkMultiBlockDataSet and vtkMultiPieceDataset, and we plan to support for variable updates +on parts within these composite datasets in a future release, but +as of 2026/02/03, that is not yet supported. + + +```text +def update_variables( + self, + dataset_id: int, + variables: list[dict[str, Any]], +) -> None +``` + +Example: +```text +visualizer = Visor() + output = converter.to_vtk_file(a_processed_file,a_processed_file_vtk) + dataset_id1 = visualizer.start(input = a_processed_file_vtk) + dataset1_variables = visualizer.list_variables(dataset_id1) + # Example assumes updating the first variable in the list + variable_to_update = dataset1_variables[0] + # Get number of points and components if needed + num_points = variable_to_update.num_points + num_components = variable_to_update.num_components + name = variable_to_update.name + # Generate new variable data as a list or numpy array + new_vector_values = np.zeros((num_points, num_components)) + # Create dictionary for variable update + variable_update_info = { + "type": "point", + "name": name, + "num_components": num_components, + "data": new_vector_values + } + # Update the variable in VISOR + visualizer.update_variables(dataset_id1, [variable_update_info]) + ### more code executed here +``` + + +#### Stop +This API stops the rendering from the Trame service, it doesn't terminate the server or the connection. If the stop method isn't called, the visualization is stopped by an interrupt when the connections and servers are killed when the lifecycle of the VISOR instance ends. + +```text + def stop() +``` + + +Example: +```text +visualizer = Visor() + output = converter.to_vtk_file(a_processed_file,a_processed_file_vtk) + visualizer.start(input = a_processed_file_vtk) + another_output = converter.to_vtk_dataset(a_processed_result, vtk_dataset) + visualizer.update(vtk_dataset) + visualizer.stop() + ### more code executed here +``` + +#### Save state + +This is a function that saves the current state of the VISOR service to a file given the filepath. + +```text +async def save_state(self, state_directory_path) +``` + +#### Load state + +This is a function that loads the current state to the VISOR viewer. + +```text +def load_state(self, state_file) +``` + + + +## References + +* trame services used are documented [here](https://trame.readthedocs.io/en/latest/viewer.server.html#). +* [wslink](https://github.com/Kitware/wslink) +* The branch that contains the above code in a PoC form is [demo](https://github.com/ansys-internal/theia/tree/demo) + + +### Notes from discussion on 14th Nov. '24 + +* Rendering engine can be changed and it should be in the initialization of the service +* Add connect_to(server) api +* Initialize using an existing server +* Save state API + +### Notes from discussion on 21st Nov '24 +* The load/save state in terms of locking when multiple users/multiple sessions are involved +* Options for RenderingEngine versus configuration like VTK / WASM / Local Rendering / Remote Rendering + + +### Notes +The API for Python applications needs to be able to start VISOR on a background thread without the user needing to manage asynchronous code from their side. The destruction of reference of the VISOR instance can signal the destruction of the VISOR instance using the atexit python library. \ No newline at end of file diff --git a/doc/developer_docs/adrs/04-jsdoc-annotations.md b/doc/developer_docs/adrs/04-jsdoc-annotations.md new file mode 100644 index 00000000..c664a690 --- /dev/null +++ b/doc/developer_docs/adrs/04-jsdoc-annotations.md @@ -0,0 +1,82 @@ +# ADR 04: JSDoc Annotations + +## Status +Proposed + +## Context +The VISOR web visualization project, in support of VTK.wasm, makes use of JavaScript to establish WebSocket connections and thereby communicate with its server-side component. By nature, JavaScript is dynamically typed, which means IDE code hinting features (e.g. Intellisense in VSCode) will often be unable to determine if written JavaScript is valid. This means that developers working in the VISOR project who rely on this feature for Python and TypeScript will be unable to do so when writing JavaScript. VISOR does have a React frontend which makes use of TypeScript (a statically typed language), although the decision to maintain "vanilla" JavaScript for the viewer WebSocket communications allows for quicker field testing and debugging, as it allows developers to skip a compilation step. + +## Decision +Ensure a reasonable amount of JSDoc annotations exist alongside JavaScript functions, classes, and other items to enable code hinting features in IDEs for developers adding to or modifying VISOR JavaScript. The JSDoc type definition names (i.e. "typedef" names) will be of the form "Visor_[type name]". For example: Visor_Vector3, Visor_WebSocketConnection, Visor_StateManager, etc. + +#### JSDoc on JavaScript Functions + +There are a multitude of ways JavaScript functions in VISOR may be annotated with JSDoc. Because functions in JavaScript can be standard declarations or expressions, corresponding JSDoc comments alongside functions may vary: + +```javascript +// example 1 + +/**@type{function(arr:any[]):number}*/ +const getArrayLength = arr => { + return arr.length; +}; +//////////////////////////////////////////////////////// +// example 2 + +/**@type{(arr:any[])=>number}*/ +const getArrayLength = arr => { + return arr.length; +}; +//////////////////////////////////////////////////////// +// example 3 + +/** + * @param {any[]} arr + * @return number + */ +function getArrayLength(arr) { + return arr.length; +} +``` + +#### JSDoc on JavaScript Objects + +Like JavaScript functions, there are several ways JavaScript objects in VISOR may be annotated with JSDoc. Unlike with functions however, JSDoc comments on objects may be slightly less straightforward: + +```javascript +// example 1 + +/** + * @typedef {Object} Visor_Vector3 + * @property {number} x - The x component. + * @property {number} y - The y component. + * @property {number} z - The z component. + */ + +/**@type{Visor_Vector3}*/ +let myVariable = null; +//////////////////////////////////////////////////////// +// example 2 + +/** + * @typedef {{x:number,y:number,z:number}} Visor_Vector3 + */ + +/**@type{Visor_Vector3}*/ +let myVariable = null; +//////////////////////////////////////////////////////// +// example 3 - note the use of the "@lends" tag + +/** + * @typedef {Object} Visor_Vector3 + */ + +let myVariable =/**@lends VISOR_Vector3#*/{ + /**@type{number}*/ + x: 0, + /**@type{number}*/ + y: 1, + /**@type{number}*/ + z: 0 +}; +``` \ No newline at end of file diff --git a/doc/developer_docs/adrs/05-states.md b/doc/developer_docs/adrs/05-states.md new file mode 100644 index 00000000..c768b319 --- /dev/null +++ b/doc/developer_docs/adrs/05-states.md @@ -0,0 +1,43 @@ +# ADR 05: Definition of States and User Cases + +## Status +Verified and Accepted by PM and ACE stakeholders + +## Context +The VISOR project shall allow for saving and restoring of states, in order to provide a smooth experience to the end user. The scope of this ADR is to define what a state is, what information it should contain, and how the restore operation should work in different deployments. + +## User case scenarios +There are three scenarios in which save and restore states will be implemented. +1. VISOR is implemented as part of a larger application. The user exits the session. When they re-enter the application, VISOR will restore the state. This will work for all deployment types (desktop, on prem and cloud). +2. The application sends to VISOR a new model to dynamically upload (streaming scenario). +3. VISOR is deployed in an on-prem or cloud application, with multiple users having access to the same project. In this scenario, VISOR should always restore the latest state for each session, regardless of which user was the last one to run VISOR. + +The consequence of the three user case scenarios outlined above is that the VISOR state files will be saved per session, and no information about which user saved the state file needs to be stored / used in the restore operation. + +**Note:** In scenario 3., there will be users with different permissions on the session - edit / read-only. In the context of the VISOR project, this is irrelevant as VISOR does not allow for modification of the model / simulation workflow itself, but only visualization. Therefore, any user who is allows to launch VISOR and load the model should be allowed to save and restore states. + +## State definition +The following information needs to be stored in a state: +1. Camera settings (look at point, look from point, rotation, zooming factor) +2. Part visibility +3. Cross section settings (on/off. Orientation and position of the cross section plane) +4. Mesh visualization (on/off) +5. Parts color by variable (on a per-part basis) +6. Legend visualization settings (hide / show, position of the legend) +7. Legend min / max +8. Legend palette +No information about the current status of the UI should be stored + +## State restore +Here we describe the expectation when restoring a state. +All the settings defined in a state should restore automatically. The UI will reset to the default UI status when you load a model for the first time. +In user case scenario 2, there is the possibility that the new model contains a different topology (part list) or a different list of variables. Here the expected behavior: +1. Missing parts: the new model has missing parts compared to the one stored in the state. Drop the information on the extra parts. This is not needed and should not be used in any way. Do not store it moving forward. +2. Extra parts: the new model has new parts compared to the one stored in the state. Visualize them with the default settings: visible and colored by a constant. + +**Note:** part matching is done based on the part name + +3. Missing variables: the new model has missing variables compared to the ones stored in the state. Drop the information on the extra variables. This is not needed and should not be used in any way. Do not store it moving forward. +4. Extra variables: the new models has new variables compared to the ones stored in the state. They will appear in the list of available variables to color a part by (when appropriate), but their settings should be the default ones. + + diff --git a/doc/developer_docs/adrs/06-qp2_compliance.md b/doc/developer_docs/adrs/06-qp2_compliance.md new file mode 100644 index 00000000..26effb1f --- /dev/null +++ b/doc/developer_docs/adrs/06-qp2_compliance.md @@ -0,0 +1,96 @@ +# ADR 06: Qp-2 compliance + +## Status +Approved + +## Context +The VISOR project follows the continuous development lifecycle of Ansys products. As such, it needs to adhere to the QP-2 guidelines. The full Quality Procedure can be found [here](https://ansys.policytech.com/dotNet/documents/?docid=1452&app=pt&source=search). + +## Responsible parties +QP-2 requires the following parties to be well defined, as person responsible for the different aspects of compliance. As pf December 2024, this is the list of people and roles. + +| Role | Person | +|--------------------|-----------------------| +| Release Manager | Palaniappan Nagappan | +| Team Lead | Marina Galvagni | +| Test Lead | Laurent Gerboud | +| Documentation Lead | Paul Coinaud | +| Product Manager | Anna Kvarnstrom | + +## QP-2 Main Requirements +In this section, we address the main requirements of QP-2 and how the project fulfills them + +### Tracebility +Both the code base and the project board of VISOR are hosted in the Ansys internal Github space. This ensures the following: +1. From a code point of view, being hosted on github allows for version control. Any audit would be able to quickly retrieve different versions of the code base and analyze the differences between versions. Single contributions are also tracked, making sure an audit could easily determine when a specific feature is entered into the code base. +2. From a project management point of view, the platform offers the Github project feature. Through this, the team can create issues, assign them, and track the code changes corresponding to each issue. Tests can also be associated with each Github issue, ensuring each feature is fully tested before release. +The Project associated with the VISOR code base can be found [here](https://github.com/orgs/ansys-internal/projects/420). + +### Correct Issue Definition +Each issue has the following mandatory fields: +1. Title: brief description of the issue +2. Description: lengthy description of the issue, with details to explain its context +3. Acceptance Criteria: a set of criteria that need to be met in order for the issue to be considered Done. + +Additional, optional fields are available to help with project management, such as iteration, links, third party software, and so on. + +Labels are used to keep track of additional information. More notabily, the following labels: +1. beta: feature not fully released to the end user. As such, it does not require testing and documentation. +2. class3: used to mark a "class 3 defect". These are hidden defects and require a special level of attention for QP-2 - see later in the Bug section. +3. maintenance / research / technical: these issues do not require testing nor documentation assodiated with them. They track, respectively: maintenance work, research spikes, and technical work (such as code refactoring) that do not have any impact on the users experience +4. documentation: issues to describe documentation work. Does not need any testing associated with it +5. test case: issue describing a test case. It will be associated with a specific functional issue. +6. test log: issue describing the results of running a test case. These are necessary only for tests that are not automated. +7. functional: issue that describles a new functionality that the user will have access to. These issues need to have documentation and testing associated with them + +There are multiple hierarchical levels of issues. While this structure isn't explicitly required by QP-2 and therefore not enforced, it helps to keep the work organized. This is done via issue types: +1. Epic: an epic is the highest level, describing a high-level functionality. A single epic can span multiple releases +2. Feature: a feature is a functionality that will be delivered in a single release cycle. It is testable and capable of adding value for the customer. +3. User Story: a single step in implementing a feature, that adds value that a user can verify independently from other user stories. It can be delived within a single iteration. +4. Task: a specific part of the work to implement a user story. Might not be testable on its own. + +### Independent review +QP-2 requires an independent review process. This is enforced in the VISOR project via the following mechanism. No user can directly push new code into the main branch (branch-protection is active). In order for the code to be merged, it needs to be reviewed and approved by at least one person who has not contribuited in the code changes. This is automatically enforced by Github. + +Moreover, issues can only be closed by someone who has not authored any of the PRs related to the issue itself. This ensures that each code change has been reviewed by an independent entity. This currently is not enforced by Github, but it is a practice inside the team. It can be verified by reviewing the history of each issue. + +### Testing +As described above, we use labels to mark test cases and test logs. Each new functionality will have tests associated with it to, and they will be executed before the release. Note that this might be done at the Feature or at the User Story level, not necessarily at both. + +A test case will contain all the instructions to reproduce the test. After being created, it needs to be reviewed by someone who is not its author. When executing it, at least one person between the author of the test case and the reviewer must not have been involved in developing the functionality that is being tested. + +The test cases can be ran manually or as part of the automated tests system. These automated tests are run nightly and before each PR is merged into the code base. Note that for automated tests, no test logs are necessary. + +### Bugs +Bugs are filed into the repository as issues with issue type: Bug. When filing a bug, the following fields are mandatory: +1. Title: short description of the issue. +2. Description: Lengthy description of the problem and how to reproduce it. +3. Severity: severity level of the bug. These are the levels: + 3a. Class 1: crash or major data loss + 3b. Class 2: Seriour problem + 3c. Class 2: Minor problem + 3d. Class 3: Hidden error. This is a separate type of bug that needs special attention (see [here](https://ansys.policytech.com/dotNet/documents/?docid=1338&app=pt&source=search)). It is reserved for bugs where the user gets a wrong output. +4. Fixed in: version that contains the bug fix + +Please note that once a bug is fixed by the developers, it goes into "Resolved" mode. It can be moved from Resolved to Done only after it has been verified by a person who was not involved in the code changes for the bug fix. This person needs to verify that the code change addresses the bug and then move the bug report from Resolved to Done. + +### OSS Usage and Security Scan +The VISOR project takes advantage of Third-Party Software components. As such, it needs to follow the procedures outlined in [QP-10](https://ansys.policytech.com/dotNet/documents/?docid=1228&app=pt&source=search) to ensure the integrated Third-Party Software satisfies functional and quality requirements. + +The Team Lead will originate the request for use of a Third-Party Software Component. Independent reviewers assigned by the Released Management Unit will review and approve the requests. This is all done via the OSS Sharepoint form. OSR reviews will therefore be recorded on this Shareport [site](https://ansys.sharepoint.com/sites/OpenSourceSoftwareTrackingIntake/Lists/OSR%20Reviewed%20Components/AllItems.aspx). + +Note that the VISOR project also takes advantage of an automated github workflow to scan third-party libraries to identify security concerns. These scans are executed nightly and at each PR push. Find more information [here](https://empowerment.dev.ansysapis.com/docs/devops/vulnerability-management/). Note that this mechanism also allows for an automatically created and retained list of Third-Party libraries used by VISOR. + +### Release +As VISOR is part of the continuous development cycle, there are no set dates for the releases. These are created on a per-need basis, balancing the needs of the team and the requests from users (internal and external to Ansys). + +In order for a release to be created, the following criteria needs to be met: +1. Stories and their tests are complete, reviewed and accepted +2. Resolved bugs are verified +3. Automated testing report has been created and published +4. Documentation is complete and reviewed +5. Regression tests have at least 90% passing rate +6. Total and priority bugs are within limits (20 bugs in total; 5 for Class 2 bugs; 0 for Class 3 bugs) +7. Third-Party Software components have been reviewed and accepted to be integrated +8. Known issues and limitations are approved +9. Legal Notices and Software Bill Of Material (SBOM) are up to date diff --git a/doc/developer_docs/adrs/07-scene-graph.md b/doc/developer_docs/adrs/07-scene-graph.md new file mode 100644 index 00000000..157732a9 --- /dev/null +++ b/doc/developer_docs/adrs/07-scene-graph.md @@ -0,0 +1,528 @@ +# ADR 07: Scene Graph + +## Status + +Proposed + +## Context + +The VISOR viewer must be aware of and preserve any object hierarchies that exist in files that are loaded. This is because the viewer must have the ability to perform actions on a single object, a custom-selected group of objects, or all descendants in a specific object's hierarchy. Examples of such actions include show, hide, select, and deselect. + +In VTK parlance, a file can represent a `vtkDataSet` or `vtkCompositeDataSet`. A `vtkDataSet` is a single polygon mesh or unstructured grid, whereas a `vtkCompositeDataSet` is a _hierarchy_ of polygon meshes or unstructured grids. + +Common subclasses of `vtkDataSet` are the `vtkPolyData` and `vtkUnstructuredGrid` types. The file extensions that typically contain these types are the following: + +- **.vtp** - `vtkPolyData` +- **.vtu** - `vtkUnstructuredGrid` + +Common subclasses of `vtkCompositeDataSet` are the `vtkMultiBlockDataSet` and `vtkMultiPieceDataSet` types. The file extensions that typically contain these types are the following (as you can see, the .vtm extension is used for both composite dataset types): + +- **.vtm** - `vtkMultiBlockDataSet` and `vtkMultiPieceDataSet` + +Because of these peculiarities among VTK datasets, VISOR requires a scene graph for management of the object hierarchies that may be present in the datasets. A scene graph is a hierarchical data structure commonly used in computer graphics and visualization to organize and manage the various objects that make up a graphical scene. It represents the spatial arrangement and relationships between objects, as well as their properties, transformations, and interactions. The scene graph allows for efficient rendering, interaction, and manipulation of complex scenes in 3D environments. + +As mentioned earlier, the VTK object types that contain hierarchies are the `vtkMultiBlockDataSet` and `vtkMultiPieceDataSet` types, which are subclasses of `vtkCompositeDataSet`. + +Although technically the `vtkPolyData` and `vtkUnstructuredGrid` types do not contain hierarchies, VISOR still treats them as being hierarchical objects, only with no children. This concept of "everything is a hierarchy" is beneficial to development, as it allows developers to normalize all scene graph methods and routines, without having to excessively make exceptions for "non-hierarchical" objects in code. + +At a high level, the following is an example of a scene graph in VISOR after loading a file named _many_blocks.vtm_: + +```text +โ— root (root) +โ””โ”€โ”€ โ— many_blocks.vtm (vtkMultiBlockDataSet) + โ”œโ”€โ”€ โ— Group A (vtkMultiBlockDataSet) + โ”‚ โ”œโ”€โ”€ โ— untitled (vtkPolyData) + โ”‚ โ”œโ”€โ”€ โ— untitled (vtkPolyData) + โ””โ”€โ”€ โ— Group B (vtkMultiPieceDataSet) + โ”œโ”€โ”€ โ— untitled (vtkPolyData) + โ”œโ”€โ”€ โ— untitled (vtkPolyData) + โ”œโ”€โ”€ โ— untitled (vtkUnstructuredGrid) + โ”œโ”€โ”€ โ— untitled (vtkUnstructuredGrid) + โ””โ”€โ”€ โ— untitled (vtkUnstructuredGrid) +``` + +As you can see, the _many_blocks.vtm_ node is a child of the _root_ node. The _root_ node is always the top-level node, and not the file node. This design decision gives us the opportunity to load multiple files into the scene, if future requirements were to demand so. + +Each node in the scene graph represents a single dataset. The dataset the node represents, however, can be "composite" or "non-composite". Composite datasets are `vtkMultiBlockDataSet` and `vtkMultiPieceDataSet`. Non-composite datasets are `vtkPolyData` and `vtkUnstructuredGrid`. + +A composite node is just a group of other nodes, and cannot be rendered in VTK _by itself_. A composite node must contain non-composite children in order to be "rendered". + +## Implementation + +The simplest way of building a scene graph from a VTK file is to design a `SceneGraphNode` class whereby its constructor takes a VTK dataset and loops through each one of its immediate child datasets. A new `SceneGraphNode` object is created for each of these child datasets by passing the child dataset to the child node. The child node's constructor then loops through each of its own child datasets and creates nodes for them as well, and for the grandchildren, and so on, until the hierarchy is completely "walked". + +As mentioned earlier, the dataset that is provided to a node's constructor will either be a composite `vtkCompositeDataSet` or non-composite `vtkDataSet`. Therefore, the node's constructor must have the ability to determine the type of dataset it was provided, so that it can set its respective class properties accordingly. These properties include `.NodeType` and `.Actor`, which are different depending on what kind of dataset the node represents. For example, a `vtkPolyData` node will have "vtkPolyData" as the `.NodeType`, and a non-null `.Actor`. Alternatively, a `vtkMultiBlockDataSet` will have "vtkMultiBlockDataSet" as the `.NodeType`, but have a null `.Actor`. + +The following is rudimentary example of a `SceneGraphNode` class, with eager loading of each `vtkActor`: + +```python +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, + vtkMultiPieceDataSet, + vtkUnstructuredGrid, + vtkPolyData, +) +from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter +from vtkmodules.vtkCommonExecutionModel import vtkPolyDataAlgorithm +import os +import re +from vtkmodules.vtkIOXML import ( + vtkXMLMultiBlockDataReader, + vtkXMLUnstructuredGridReader, + vtkXMLPolyDataReader, +) +from vtkmodules.vtkCommonDataModel import vtkCompositeDataSet, vtkDataSet + + +class SceneGraphNode: + def __init__(self, dataset: vtkDataSet | vtkCompositeDataSet): + node_type: str + actor: vtkActor | None = None + children: list[SceneGraphNode] = [] + if isinstance(dataset, vtkMultiBlockDataSet): + node_type = "vtkMultiBlockDataSet" + for i in range(dataset.GetNumberOfBlocks()): + child = dataset.GetBlock(i) + item = SceneGraphNode(child) + children.append(item) + elif isinstance(dataset, vtkMultiPieceDataSet): + node_type = "vtkMultiPieceDataSet" + for i in range(dataset.GetNumberOfPieces()): + child = dataset.GetBlock(i) + item = SceneGraphNode(child) + children.append(item) + elif isinstance(dataset, vtkUnstructuredGrid): + node_type = "vtkUnstructuredGrid" + algorithm: vtkPolyDataAlgorithm = vtkGeometryFilter() + algorithm.SetInputData(dataset) + algorithm.Update(None) + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(algorithm.GetOutputPort()) + actor = vtkActor() + actor.SetMapper(mapper) + elif isinstance(dataset, vtkPolyData): + node_type = "vtkPolyData" + mapper = vtkPolyDataMapper() + mapper.SetInputData(dataset) + actor = vtkActor() + actor.SetMapper(mapper) + else: + raise RuntimeError(f"dataset type not yet supported: {type(dataset)}") + self.__NodeType: str = node_type + self.__Actor: vtkActor | None = actor + self.__Children: list[SceneGraphNode] = children + + @property + def NodeType(self): + return self.__NodeType + + @property + def Actor(self): + return self.__Actor + + @property + def Children(self): + return self.__Children + + +def file_to_dataset(file_path: str) -> vtkDataSet | vtkCompositeDataSet: + """""" + # make file_path lowercase so extension testing is case-insensitive + filename: str = os.path.basename(file_path).lower() + extension: str = os.path.splitext(filename)[1][1:] + dataset: vtkDataSet | vtkCompositeDataSet + if extension == "vtu": + """""" + reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + elif extension == "vtp": + """""" + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + elif extension == "vtm": + """""" + reader: vtkXMLMultiBlockDataReader = vtkXMLMultiBlockDataReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + else: + """""" + raise RuntimeError(f"Unsupported file: {file_path}") + return dataset + + +def main(): + dataset = file_to_dataset("c:/path/to/file.vtm") + root_node = SceneGraphNode(dataset) +``` + +There are interesting things to note with this example. First of all, notice that if the node represents a `vtkUnstructuredGrid` or `vtkPolyData`, its `.Children` property will be empty. Secondly, notice that if the node represents a `vtkMultiBlockDataSet` or `vtkMultiPieceDataSet`, its `.Actor` property will be `None`. This follows the design principle mentioned earlier whereby composite datasets cannot be rendered on their own (because they have no actor), in addition to non-composite objects still being treated as hierarchical, just with 0 children. + +It is worth mentioning that there are limitations in this rudimentary `SceneGraphNode` example. For example, there is no way to update each node's pipeline at runtime from outside the class (i.e., you cannot add extra algorithms to the VTK pipeline before the initial dataset is handed over to a `vtkMapper`). Secondly, there is no convenient way to access all of a node's descendants (i.e. there is only a `.Children` array, which is just a node's immediate children, and does not include grandchildren, great-grandchildren, and so on). These limitations and solutions are discussed in the next two sections. + +## Runtime VTK Algorithm Pipeline Modding + +In order to improve the scalability of the `SceneGraphNode` class, each node's pipeline should be changeable from outside the class. In the rudimentary `SceneGraphNode` code example shown earlier, each node's base dataset is directly converted to a `vtkActor`. In other words, there is no ability to inject "middleware" to the pipeline before the dataset is sent to the `vtkActor`. + +We can change this by introducing the ability to provide a function parameter to each node, whereby the function is given a `vtkAlgorithm`, and returns a `vtkAlgorithm`. The updated code for this is as follows (see the method `.UpdateDescendantOrSelfActors()`): + +```python +from vtkmodules.vtkFiltersCore import vtkAppendPolyData +from typing import Callable +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, + vtkMultiPieceDataSet, + vtkUnstructuredGrid, + vtkPolyData, +) +from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter +from vtkmodules.vtkCommonExecutionModel import vtkPolyDataAlgorithm +import os +import re +from vtkmodules.vtkIOXML import ( + vtkXMLMultiBlockDataReader, + vtkXMLUnstructuredGridReader, + vtkXMLPolyDataReader, +) +from vtkmodules.vtkCommonDataModel import vtkCompositeDataSet, vtkDataSet +from vtkmodules.vtkFiltersModeling import vtkLoopSubdivisionFilter + + +class SceneGraphNode: + def __init__(self, dataset: vtkDataSet | vtkCompositeDataSet): + node_type: str + base_algorithm: vtkPolyDataAlgorithm | None = None + actor: vtkActor | None = None + children: list[SceneGraphNode] = [] + if isinstance(dataset, vtkMultiBlockDataSet): + node_type = "vtkMultiBlockDataSet" + for i in range(dataset.GetNumberOfBlocks()): + child = dataset.GetBlock(i) + item = SceneGraphNode(child) + children.append(item) + elif isinstance(dataset, vtkMultiPieceDataSet): + node_type = "vtkMultiPieceDataSet" + for i in range(dataset.GetNumberOfPieces()): + child = dataset.GetBlock(i) + item = SceneGraphNode(child) + children.append(item) + elif isinstance(dataset, vtkUnstructuredGrid): + node_type = "vtkUnstructuredGrid" + base_algorithm = vtkGeometryFilter() + base_algorithm.SetInputData(dataset) + base_algorithm.Update(None) + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(base_algorithm.GetOutputPort()) + actor = vtkActor() + actor.SetMapper(mapper) + elif isinstance(dataset, vtkPolyData): + node_type = "vtkPolyData" + base_algorithm = vtkAppendPolyData() + base_algorithm.SetInputData(dataset) + mapper = vtkPolyDataMapper() + mapper.SetInputData(dataset) + actor = vtkActor() + actor.SetMapper(mapper) + else: + raise RuntimeError(f"dataset type not yet supported: {type(dataset)}") + self.__NodeType: str = node_type + self.__BaseAlgorithm: vtkPolyDataAlgorithm | None = base_algorithm + self.__Actor: vtkActor | None = actor + self.__Children: list[SceneGraphNode] = children + + @property + def NodeType(self): + return self.__NodeType + + @property + def Actor(self): + return self.__Actor + + @property + def Children(self): + return self.__Children + + def UpdateDescendantOrSelfActors( + self, algorithm_filter: Callable[[vtkPolyDataAlgorithm], vtkPolyDataAlgorithm] + ): + if self.__BaseAlgorithm is None: + # if base algorithm is not present, then this is a composite node + # therefore loop through all the children with the algorithm filter + for node in self.__Children: + node.UpdateDescendantOrSelfActors(algorithm_filter) + else: + # if base algorithm is present, then this is an actual mesh node + # therefore update the mapper with the new algorithm (and thus + # the actor) + mapper = self.__Actor.GetMapper() + if isinstance(mapper, vtkPolyDataMapper): + algorithm = algorithm_filter(self.__BaseAlgorithm) + mapper.SetInputConnection(algorithm.GetOutputPort()) + else: + raise RuntimeError( + f"mapper is not vtkPolyDataMapper. actual type: {type(mapper)}" + ) + + +def file_to_dataset(file_path: str) -> vtkDataSet | vtkCompositeDataSet: + """""" + # make file_path lowercase so extension testing is case-insensitive + filename: str = os.path.basename(file_path).lower() + extension: str = os.path.splitext(filename)[1][1:] + dataset: vtkDataSet | vtkCompositeDataSet + if extension == "vtu": + """""" + reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + elif extension == "vtp": + """""" + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + elif extension == "vtm": + """""" + reader: vtkXMLMultiBlockDataReader = vtkXMLMultiBlockDataReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + else: + """""" + raise RuntimeError(f"Unsupported file: {file_path}") + return dataset + + +def main(): + dataset = file_to_dataset("c:/path/to/file.vtm") + root_node = SceneGraphNode(dataset) + + def algorithm_filter(algorithm: vtkPolyDataAlgorithm): + subdivide: vtkPolyDataAlgorithm = vtkLoopSubdivisionFilter() + subdivide.SetInputConnection(algorithm.GetOutputPort()) + return subdivide + + root_node.UpdateDescendantOrSelfActors(algorithm_filter) +``` + +Notice the addition of `base_algorithm = vtkAppendPolyData()` to the `vtkPolyData` match case in the node constructor. This algorithm serves as a "pass-through" filter to allow us to use our `vtkPolyData` object as an algorithm. + +Lastly, notice the `algorithm_filter` function passed to the `.UpdateDescendantOrSelfActors()` method. This function argument will be applied to every descendant node under the root node (since we called the method on the root node). + +## Iterate All Node Descendants (not just immediate children) + +At this point, iterating through the immediate children of a node is straightforward: + +```python +scene_root = SceneGraphNode(dataset) +for node in scene_root.Children: + print(f"node type: {node.NodeType}") +``` + +However, iterating through ALL descendants of a node requires a function definition to be called recursively: + +```python +def recursive_func(node: SceneGraphNode): + print(f"node type: {node.NodeType}") + for node in node.Children: + recursive_func(node) + + +scene_root = SceneGraphNode(dataset) +recursive_func(scene_root) +``` + +Alternatively, we could attach a special array and dictionary to each node to make each node's descendants much easier to iterate. See the following code for this functionality: + +```python +from vtkmodules.vtkFiltersCore import vtkAppendPolyData +from typing import Callable +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, + vtkMultiPieceDataSet, + vtkUnstructuredGrid, + vtkPolyData, +) +from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter +from vtkmodules.vtkCommonExecutionModel import vtkPolyDataAlgorithm +import os +import re +from vtkmodules.vtkIOXML import ( + vtkXMLMultiBlockDataReader, + vtkXMLUnstructuredGridReader, + vtkXMLPolyDataReader, +) +from vtkmodules.vtkCommonDataModel import vtkCompositeDataSet, vtkDataSet +from vtkmodules.vtkFiltersModeling import vtkLoopSubdivisionFilter +import random + + +class SceneGraphNode: + def __init__(self, dataset: vtkDataSet | vtkCompositeDataSet): + node_type: str + base_algorithm: vtkPolyDataAlgorithm | None = None + actor: vtkActor | None = None + children: list[SceneGraphNode] = [] + # in case this node gets serialized to JSON and used in JavaScript, + # limit the maximum value to 9007199254740991, since this is + # JavaScript's maximum safe integer + javascript_safe_id: int = random.randint(1000000000000000, 9007199254740991) + descendantNodesOrSelfDictionary: dict[int, SceneGraphNode] = { + javascript_safe_id: self + } + descendantNodesOrSelfArray: list[SceneGraphNode] = [self] + self.__DescendantNodesOrSelfDictionary: dict[ + int, SceneGraphNode + ] = descendantNodesOrSelfDictionary + self.__DescendantNodesOrSelfArray: list[ + SceneGraphNode + ] = descendantNodesOrSelfArray + if isinstance(dataset, vtkMultiBlockDataSet): + node_type = "vtkMultiBlockDataSet" + for i in range(dataset.GetNumberOfBlocks()): + child = dataset.GetBlock(i) + item = SceneGraphNode(child) + children.append(item) + descendantNodesOrSelfDictionary.update( + item.DescendantNodesOrSelfDictionary + ) + descendantNodesOrSelfArray.extend(item.DescendantNodesOrSelfArray) + elif isinstance(dataset, vtkMultiPieceDataSet): + node_type = "vtkMultiPieceDataSet" + for i in range(dataset.GetNumberOfPieces()): + child = dataset.GetBlock(i) + item = SceneGraphNode(child) + children.append(item) + descendantNodesOrSelfDictionary.update( + item.DescendantNodesOrSelfDictionary + ) + descendantNodesOrSelfArray.extend(item.DescendantNodesOrSelfArray) + elif isinstance(dataset, vtkUnstructuredGrid): + node_type = "vtkUnstructuredGrid" + base_algorithm = vtkGeometryFilter() + base_algorithm.SetInputData(dataset) + base_algorithm.Update(None) + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(base_algorithm.GetOutputPort()) + actor = vtkActor() + actor.SetMapper(mapper) + elif isinstance(dataset, vtkPolyData): + node_type = "vtkPolyData" + base_algorithm = vtkAppendPolyData() + base_algorithm.SetInputData(dataset) + mapper = vtkPolyDataMapper() + mapper.SetInputData(dataset) + actor = vtkActor() + actor.SetMapper(mapper) + else: + raise RuntimeError(f"dataset type not yet supported: {type(dataset)}") + self.__NodeType: str = node_type + self.__BaseAlgorithm: vtkPolyDataAlgorithm | None = base_algorithm + self.__Actor: vtkActor | None = actor + self.__Children: list[SceneGraphNode] = children + + @property + def DescendantNodesOrSelfDictionary(self): + return self.__DescendantNodesOrSelfDictionary + + @property + def DescendantNodesOrSelfArray(self): + return self.__DescendantNodesOrSelfArray + + @property + def NodeType(self): + return self.__NodeType + + @property + def Actor(self): + return self.__Actor + + @property + def Children(self): + return self.__Children + + def UpdateDescendantOrSelfActors( + self, algorithm_filter: Callable[[vtkPolyDataAlgorithm], vtkPolyDataAlgorithm] + ): + if self.__BaseAlgorithm is None: + # if base algorithm is not present, then this is a composite node + # therefore loop through all the children with the algorithm filter + for node in self.__Children: + node.UpdateDescendantOrSelfActors(algorithm_filter) + else: + # if base algorithm is present, then this is an actual mesh node + # therefore update the mapper with the new algorithm (and thus + # the actor) + mapper = self.__Actor.GetMapper() + if isinstance(mapper, vtkPolyDataMapper): + algorithm = algorithm_filter(self.__BaseAlgorithm) + mapper.SetInputConnection(algorithm.GetOutputPort()) + else: + raise RuntimeError( + f"mapper is not vtkPolyDataMapper. actual type: {type(mapper)}" + ) + + +def file_to_dataset(file_path: str) -> vtkDataSet | vtkCompositeDataSet: + """""" + # make file_path lowercase so extension testing is case-insensitive + filename: str = os.path.basename(file_path).lower() + extension: str = os.path.splitext(filename)[1][1:] + dataset: vtkDataSet | vtkCompositeDataSet + if extension == "vtu": + """""" + reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + elif extension == "vtp": + """""" + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + elif extension == "vtm": + """""" + reader: vtkXMLMultiBlockDataReader = vtkXMLMultiBlockDataReader() + reader.SetFileName(file_path) + reader.Update() + dataset = reader.GetOutput() + else: + """""" + raise RuntimeError(f"Unsupported file: {file_path}") + return dataset + + +def main(): + dataset = file_to_dataset("c:/path/to/file.vtm") + root_node = SceneGraphNode(dataset) + + def algorithm_filter(algorithm: vtkPolyDataAlgorithm): + subdivide: vtkPolyDataAlgorithm = vtkLoopSubdivisionFilter() + subdivide.SetInputConnection(algorithm.GetOutputPort()) + return subdivide + + root_node.UpdateDescendantOrSelfActors(algorithm_filter) +``` + +The property `DescendantNodesOrSelfDictionary` is a dictionary that contains all descendant nodes of a node PLUS the node it was called from. The key is a random integer, and the value is the descendant node. + +Notice that the name of the properties `DescendantNodesOrSelfDictionary` and `DescendantNodesOrSelfArray` include the phrase "OrSelf". This is because the content of these collections depends on whether the node they are accessed from is a composite or non-composite node. If it is a composite node, the dictionary and array contain all descendant nodes of the node the property was accessed from PLUS the node the properties were accessed from. If the node the property was accessed from is a non-composite node, then the collections contain ONLY the node that the property was accessed from. + +We can now shorten our iteration code to the following: + +```python +scene_root = SceneGraphNode(dataset) +for node in scene_root.DescendantNodesOrSelfArray: + print(f"node type: {node.NodeType}") +``` + +The code above will iterate through all descendants of the scene root, BUT the first element in the collection will be the scene root itself. The same goes for the dictionary as well. \ No newline at end of file diff --git a/doc/developer_docs/adrs/08-awc-decision.md b/doc/developer_docs/adrs/08-awc-decision.md new file mode 100644 index 00000000..02c547a2 --- /dev/null +++ b/doc/developer_docs/adrs/08-awc-decision.md @@ -0,0 +1,38 @@ +# ADR 08: AWC adoption + +## Status + +Team agreement + +## Context + +In the initial implementation of VISOR, the Ansys Web Components (AWCs) were used to create the front end features. This has been done with the following goals in mind: +- make the look and feel of VISOR common with the rest of Ansys products. This is especially important considering that VISOR is a Shared Technology Component, to be embedded inside other Ansys frameworks; +- reduce technical debt moving forward. Once the AWCs are used in VISOR, changes in the Ansys guidelines for the UI / UX would result in a simple component update, without having to have the team re-write all the UI elements; +- Outsource the UI work to another team. As the VISOR team is small, we'd like to outsource as many components as possible. + +The team therefore started the project using AWCs for the front end. + +## Issues with AWCs adoption + +We list a number of issues the team has encountered when using the AWCs. + +- Lack of support for new Dash versions<br> +AWCs currently fully support previous version of Dash (2.6 released in August 2022), and VISOR requires newer versions of Dash (>2.16, but preferably 3.0.1) due to the usage of JacaSacript Modules in the Trame Client, which is used in the VISOR client. This is a crucial technology choice which requires WebAssembly files and JS Modules (mjs) files. This means that the VISOR integration inside of SAF can not be done with UI elements if they are developed using AWCs. + +- Non React-native: size and performance<br> +AWCs are available both in Angular and React. Upon further investigation, though, it appears that the React components are not native, but are obtained via a translation of the native Angular components. This creates performance and memory issues. The React AWCs tend to be very big in size and when VISOR needs to bundle all of its components and wrap them for its own bundle but even further the custom Dash component, the end result is excessively big in size and creates complications for the bundling of the VISOR client React component. The VISOR client already has to bundle in Dash the WebAssembly and JS Module files and adding excessive additional load to our bundle is creating issues that are ending up in loading times. At this point in time we have not bundled the AWCs in our dash component due to the fact that we would need to invest time to optimize and drive down the size of our bundle for it to be in acceptable sizes. + +- Lack or personalization and optimization options<br> +AWCs appear to be designed to be used 'as is' in a Solution Application. But this is not VISOR's user case. VISOR currently is only focusing on MVP functionalities but going forward there are more UI elements that are going to be needing further customization, such as elements related to time variant domains and animation capabilities, which aren't commonly required in other tools. The customization levels are going to be even more prevalent as the project expands and the UI elements we are going to be loading on screen are going to be needing high optimization so that they don't take up the memory budget we require for loading meshes. Without the alignment of high performance React native elements it will not be possible for VISOR long term to be able to use them. In the memory budget we have on the browser we are required to optimize for providing as much as possible of that budget to the graphics engine and the browser to load complicated and the biggest meshes possible so we are bound to drive down the cost of UI elements as much as possible in the long term. The more elements we use this is going to become an even bigger issue and since this is a very specialized visualization application the consistency is actually compared to other Ansys applications which also have their own specialized elements. + +- Low prioritization of missing features and bugs<br> +Some pretty basic features for VISOR's use case seem to be missing from AWC components. As the AWC team focuses mainly on the Angular version, little prioritization is given to our requests. For example, see the issues raised:<br>https://github.com/ansys-internal/ansys-web-components/issues/2156<br>The bugs associated with the issue have been filed 3 weeks after the initial report and given low or medium priority. The lack of responsiveness creates a dependency that is hard to accept and justify. These discussionss with the AWC team show that the requirements that VISOR provides are not necessarily aligned with the scope of AWCs. We require AWC which are fully customizable by other teams and able to be integrated in React applications, not necessarily used 'as is' in a Solution Application. + +- Rejection of features<br> +Some features we've requested to the AWC team have been rejected, even through we feel they're basic requests that VISOR can not stay without. For example, see item 1. in this discussion:<br>https://github.com/ansys-internal/ansys-web-components/issues/2156<br>The request to have a tree (for the part list) that adapts in size with the length of the part names is rejected. We therefore are left with a very large UI component that occupies almost 1/2 of the rendering window even if the text of the part list is small. Similarly, a request to control the padding of the strings is rejected, leaving us with UI components way too large. + +## Decision + +Given the issues listed in this ADR, the team has decided to abandon the AWCs for VISOR's front end and will be looking at having its own specialized elements. We remain open to discussion in the future if the project's requirement were to re-align with AWCs target. We also remain open to the possibility of alternative ways to achieve consistency with other Ansys products such as Theme libraries and shared CSS resources, as well as guidelines which allow for common look and feel even through the elements are implemented using different technologies. + diff --git a/doc/developer_docs/adrs/09-visor-trame-logging.md b/doc/developer_docs/adrs/09-visor-trame-logging.md new file mode 100644 index 00000000..df70e2bc --- /dev/null +++ b/doc/developer_docs/adrs/09-visor-trame-logging.md @@ -0,0 +1,350 @@ + +# ADR 09: VISOR Logging + +## Table of Contents +- [Decision](#decision) +- [Context](#context) + - [Unifying Logging in VISOR](#unifying-logging-in-visor) + - [Adding Trame Logs in VISOR](#adding-trame-logging-in-visor) +- [Proposed Changes](#proposed-changes) + - [Unify Logging](#unify-logging) + - [Trame Logging](#trame-logging) +- [Example Log Output](#example-log-output) +- [Observability Compliance](#observability-compliance) +- [Related Issues](#related-issues) + + + +## Decision + +* Unify the VISOR Python logging by implementing a custom VisorLogger class and +using it throughout the project. +* Add a default log directory where all Python logs are stored, configured in the `config.Settings` class. +* Allow a user to enable additional trame logging to a customizable log location, by adding a keyword +argument to the VISOR class constructor. In standalone VISOR, this is configured in the `config.Settings` +class and passed to the VISOR class upon instantiation. + +## Context + + +### Unifying Logging in VISOR + +Logging in the VISOR Python code is currently configured separately in individual VISOR viewer modules. Most of the +modules set up logging something like the following example code from `application.py`: +```angular2html +import logging +logger = logging.getLogger(__name__) +log_path = path.join(curdir, "logs") +from pathlib import Path + +Path(log_path).mkdir(parents=True, exist_ok=True) +file = path.join(log_path, "visor.log") +logging.basicConfig(filename=file, encoding="utf-8", level=logging.DEBUG) +``` + +There are a few reasons why we would benefit from centralizing this code for consistency across the project. +1. **Adds consistency in logging across the project**: This would allow our logs to be consistent in the log naming, +output location, formatting, and log level. +2. **Simplifies logging setup**: Easier to set up logging by utilizing reusable components. +3. **Adds clarity in logging practices**: Having centralized logging in the project allows us to more +easily evaluate and make changes to to comply with Ansys standards. + + +### Adding Trame Logging in VISOR + +In addition to the existing logging in VISOR, a user may want extra log info coming from Trame. + +We would like to allow a user to optionally enable additional logging about the Trame server, and for +them to be able to select the output location where those logs are written. + +By default, this option would be disabled. + + + +## Proposed Changes + +### Unify Logging +We can create a VisorLogger subclass of the Python logging.Logger class, where the file handling is centralized, and +the default logging level and format are defined. + +We can define a default log directory within the application `config.Settings` class as follows: +```angular2html +from pathlib import Path +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + app_name: str = "VISOR Viewer" + default_host: str = "localhost" + default_port: int = 8081 + default_standalone: bool = True + default_log_dir: str = str(Path.cwd().joinpath("logs")) # New setting +``` + +The following VisorLogging class can use the `default_log_dir` as a default if no other directory is set. +```angular2html +"""Logging configuration""" +import logging +from logging import Logger +from pathlib import Path + +from ansys.visor.viewer.config import Settings + +class VisorLogger(Logger): + """ + Custom logger for the VISOR app. + This logger writes logs to a file, allows setting the log level, + and ensures the log directory exists. + Args: + name (str): The name of the logger, typically the module or class name. + filename (str): The name of the log file. + log_dir (Optional[str]): Directory to save logs. + Default is the default_log_dir from config settings. + level (int): Logging level. Default is logging.DEBUG. + """ + + # Logging format to comply with Ansys ADR: + # https://github.com/ansys-internal/architecture-decision-records/blob/main/content/docs/adrs/0016-observability-strategy.md + LOGGING_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d %(funcName)s()] - %(message)s" + ENCODING = "utf-8" + + def __init__(self, + name: str, + filename: str, + log_dir: str | None = None, + level: int =logging.DEBUG): + # Initialize the parent class + super().__init__(name, level) + + # Log level + self.level = level + + # Set up the log directory and file path + self.filename = filename + self.log_dir = log_dir + if log_dir is None: + settings = Settings() + self.log_dir = settings.default_log_dir + self.file_path = self.get_file_path() + + # Ensure the log directory exists + self.create_dir() + + # Configure the root logger via basicConfig + # (this will affect any logger that doesn't have a handler) + logging.basicConfig( + level=level, + format=self.LOGGING_FORMAT, + handlers=[ + logging.FileHandler( + self.file_path, + encoding=self.ENCODING + ) + ], + ) + + # Create file handler and set logging level + file_handler = logging.FileHandler( + self.file_path, + encoding=self.ENCODING + ) + file_handler.setLevel(level) + + # Add formatter for file handler + formatter = logging.Formatter(self.LOGGING_FORMAT) + file_handler.setFormatter(formatter) + + self.addHandler(file_handler) + + # Prevent propagation to the root logger + self.propagate = False + + def create_dir(self) -> None: + Path(self.log_dir).mkdir(parents=True, exist_ok=True) + + def get_file_path(self) -> Path: + return Path(self.log_dir).joinpath(self.filename) +``` + +For convenience, we can also create a subclass of the above that uses the default log directory from the project settings, +and writes to a file called `visor.log`. + +```angular2html +class VisorDefaultLogger(VisorLogger): + """ + Custom logger for VISOR app with a default log file. + This logger writes to a fixed log file named "visor.log", ensuring + consistency across multiple modules within the project. + It inherits from the VisorLogger class, which allows for centralized + configuration and logging. + This default logger is intended for logging all of the project-related + messages to the same log file across different modules while maintaining + a consistent logging format and level. + Args: + name (str): The name of the logger, typically the module or class name. + """ + def __init__(self, name): + # Initialize the parent class + super().__init__(name, "visor.log") +``` + + +To summarize: + +* Create a `default_log_dir` in the `Settings` class, which a user will configure for +the needs of their application. +* Create a `VisorLogger` subclass of the Python `logging.Logger` class, which sets up logging to a file, +allows setting the log level, and ensures the log directory exists. If a log directory is not specified, use the +default_log_dir from the Settings class. + * Note that within `VisorLogger` the `basicConfig` is configured, which enables any logger that doesn't + have a handler to continue to write to the specified log file even if it is not using the logger + explicitly (e.g. the trame logger currently, but any other framework that does logging under the + hood will be captured by this too). +* Created a `VisorDefaultLogger` subclass of `VisorLogger` which takes only the logger name +(usually the file name) as input, and writes out to a file called `visor.log` in the `default_log_dir` directory. This class is a convenience that was created to simplify and unify the logging across modules. +* Use `VisorDefaultLogger` in most of the viewer modules (anywhere that had `visor.log` specified as the +output log file previously). +* Use `VisorLogger` to log to `server.log` + + +**Substantive changes**: The changes above should be mostly invisible to the user. However, the following will be different: +* Logging format across all files +* Ability to set the log output directory in `config.Settings` +* Output directory for `server_instances.log` is now the `default_log_dir` +(this used to be under `src\ansys\visor\viewer\logs`) + + +### Trame Logging + +We propose the following implementation. +1. Expose a `trame_log_dir` setting in `config.Settings`, which is by default set to None, but when set, +turns on Trame logging which will write the output log files to this directory. +2. Application logs: `visor_trame_app.log` + * **Direct Trame's native Python logs to a custom file**: + Trame uses Python's `logging` library to log using logger names `trame`, `trame_server`, and `trame.app`. + By default, these are written out to `visor.log`, but we can also capture these and redirect them to + a separate Trame application log file using the Python `logging` library. + * **Lifecycle hooks**: Trame offers hooks that can be added about the Trame server lifecycle + (e.g. `on_server_start`, `on_client_exited`). We can log these under a `trame_lifecycle` logger name + and write to the same log file as above. +3. Network logs: `visor_trame_network.log` + * The Trame server has an optional keyword argument `log_network` + (see the [Trame docs](https://trame.readthedocs.io/en/latest/core.server.html)), + which is False by default, but when set to a path to a log file, will write out + additional logs to that file. This logs communication between Python and the frontend. + + +Note that the trame log dir is configurable, but the trame log names are fixed. + + + +## Example Log Output + +--- +1. application logs via `trame.logger` and lifecycle hooks: provides information about the application lifecycle (tracking when the server start, updates, ends). The output looks like e.g. +``` +2025-05-01 08:09:26,050 - trame.decorators.klass - DEBUG - Instance created +2025-05-01 08:09:26,050 - trame.decorators.klass - DEBUG - server=<trame_server.core.Server object at 0x00000241F7A876A0> prefix='' +2025-05-01 08:09:26,050 - trame.decorators.klass - DEBUG - state.change(['plane_widget'])(_on_widget_update) +2025-05-01 08:09:26,050 - trame.decorators.klass - DEBUG - trigger(get_scene_graph_json)(get_scene_graph_json) +2025-05-01 08:09:26,051 - trame.decorators.klass - DEBUG - trigger(node_hide)(node_hide) +2025-05-01 08:09:26,051 - trame.decorators.klass - DEBUG - trigger(node_show)(node_show) +2025-05-01 08:09:26,051 - trame.decorators.klass - DEBUG - trigger(toggle_cross_section)(toggle_cross_section) +2025-05-01 08:09:26,051 - trame.decorators.klass - DEBUG - trigger(toggle_wireframe)(toggle_wireframe) +2025-05-01 08:09:26,051 - trame.decorators.klass - DEBUG - trigger(update_selection)(update_selection) +2025-05-06 15:14:02,280 - trame_server.controller - INFO - [controller.py:70 register_trigger()] - trigger(update_selection) +2025-05-06 15:39:42,278 - trame_lifecycle - DEBUG - [application.py:163 server_ready()] - Server is ready. +2025-05-06 15:39:43,113 - trame_lifecycle - DEBUG - [application.py:168 client_connected()] - Client connected. +2025-05-06 15:39:46,232 - trame_lifecycle - DEBUG - [application.py:173 client_exited()] - Client exited. +2025-05-06 15:39:49,595 - trame_lifecycle - DEBUG - [application.py:168 client_connected()] - Client connected. +2025-05-06 15:39:51,736 - trame_lifecycle - DEBUG - [application.py:173 client_exited()] - Client exited. +2025-05-06 15:39:54,949 - trame_lifecycle - DEBUG - [application.py:178 server_exited()] - Server is exiting. +2025-05-06 15:39:57,494 - trame_lifecycle - DEBUG - [application.py:163 server_ready()] - Server is ready. +2025-05-06 15:39:59,178 - trame_lifecycle - DEBUG - [application.py:168 client_connected()] - Client connected. +2025-05-06 15:40:06,227 - trame_lifecycle - DEBUG - [application.py:178 server_exited()] - Server is exiting. +``` + + +2. The log_network Server option provides logs of communication between Python and the frontend, e.g. +``` +----------- STATE: Client => Server ----------- +[ + { + "key": "trame__busy", + "value": 0 + } +] +------------------------------------------------------------ +----------- STATE: Server => Client ----------- +{ + "trame__busy": 0 +} +------------------------------------------------------------ +----------- EVENT: Client => Server ----------- +{ + "name": "get_scene_graph_json", + "args": [], + "kwargs": {} +} +------------------------------------------------------------ +----------- EVENT: Client => Server ----------- +{ + "name": "node_show", + "args": [ + 7601913218618099 + ], + "kwargs": {} +} +------------------------------------------------------------ +----------- EVENT: Client => Server ----------- +{ + "name": "node_show", + "args": [ + 3465350337367369 + ], + "kwargs": {} +} +``` +## Observability Compliance + +ADR [#16](https://github.com/ansys-internal/architecture-decision-records/blob/main/content/docs/adrs/0016-observability-strategy.md) +outlines observability requirements on an Ansys level. + +We have logs, but no traces or metrics implemented yet. Traces are required for all applications / services +that are part of a distributed / microservices architecture, so we will need OpenTelemetry integration in VISOR. +This is in our backlog ([#94](https://github.com/ansys-internal/theia/issues/94)), +and we would like to take steps to move closer to this. + +After unifying the main logging mechanism in VISOR, we will have made a couple of improvements bringing us +closer to the logging requirements. + +| Requirement | Current | Unified Logs | +|-------------------------------------------------------------------------|-------------------------------|-------| +| [Required] Logs | โœ… | โœ… | +| [Required] Logs are structured, well formatted | โœ… | โœ… | +| [Required] Logs generate high severity log events (ERROR, FATAL) | โœ… | โœ… | +| [Required] Logs in distributed envs are JSON formatted | โŒ | โŒ | +| [Required] Logs able to change min severity level through configuration | Yes, but not in one place | โœ… | +| [Required Field] Message | โœ… | โœ… | +| [Required Field] LoggerName | โœ… | โœ… | +| [Required Field] Level | โœ… | โœ… | +| [Required Field] Timestamp | โŒ | โœ… | +| [Recommended Field] LineNo | โŒ | โœ… | +| [Recommended Field] FileName/Class/Module | โŒ | โœ… | +| Traces* | โŒ | โŒ | +| Metrics | โŒ | โŒ | +| [Required Field] TraceId | N/A (until traces implemented) | N/A (until traces implemented) | +| [Required Field] SpanId | N/A (until traces implemented) | N/A (until traces implemented) | +| [Required Field] ServiceName | N/A (until traces implemented) | N/A (until traces implemented) | + +\* Because do not have traces implemented yet, we can't yet to add the TraceId, SpandId, or ServiceName in our logs. + + +## Related Issues + +Two issues related to this topic are here: +* [#251](https://github.com/ansys-internal/theia/issues/251) +Unify logging mechanisms +(PR [#252](https://github.com/ansys-internal/theia/pull/252)) +* [#236](https://github.com/ansys-internal/theia/issues/236) +Create logging mechanism for Trame server in VISOR +(PR [239](https://github.com/ansys-internal/theia/pull/239)) diff --git a/doc/developer_docs/adrs/10-visor-http-api.md b/doc/developer_docs/adrs/10-visor-http-api.md new file mode 100644 index 00000000..e6709964 --- /dev/null +++ b/doc/developer_docs/adrs/10-visor-http-api.md @@ -0,0 +1,475 @@ +# ADR 10: VISOR RESTful Service + +## Status +Decided + +## Context +VISOR is the visualization component for Solutions Applications Framework. VISOR is used a service through the PIM configuration management from SAF. This service is managed by SAF Product Instance Manager (PIM) in terms of its lifecycle and in that way its configuration allows it to be used by SAF engineers through the REST interface it provides. + +## VISOR Service + +The VISOR service currently only supports an infrastructure based on Trame client-side rendering through VTK.WASM technology. This Trame framework efficiently only supports only one session and the authentication and authorization for the session is handled outside of VISOR. The client-server connection is based on ws-link which creates a WebSocket connection from the server to the client browser of the user. Management of files for VISOR currently only support loading in memory from a local storage unit in order to create an internal representation in memory based on a scene-graph. The VTK pipeline is setup on the server and the final stage is transferred to the client where it will locally manage user interaction on an optimistic mechanism that most of the processes can be serialized through its architecture and locally caching and computation will only be transferred to the server in terms of state management through the internal VISOR mechanism. + +VISOR service can save its state and load from its previous state. The way the current VISOR service is managed is shown from the following sequence diagram. + + +#### Sequence Diagram + + +```mermaid + sequenceDiagram + Visor_instance->>Trame_server: start service + Trame_server->>wslink: start + wslink-->>Trame_server: wslink started successfully + Trame_server-->>Visor_instance: Trame server started successfully + Visor_instance->>Trame_server: update state and rendering + Trame_server-->Server.State: update state 'input_file' + Visor_instance-->VTK_Local_Rendering: update VTK pipeline + VTK_Local_Rendering-->wslink: update rendering + Visor_instance->>Trame_server: stop server + Trame_server->>wslink: wslink.stop() + wslink-->Tram_server: wslink has stopped successfully + Trame_server-->Visor_instance: trame server has stopped successfully +``` + + +### REST API + + VISOR's REST API is following the OpenAPI specification and is versioned with the same version as VISOR (it doesn't have an independent versioning scheme) from the rest of VISOR and the VISOR Python API. + +### GET / +The get root endpoint returns the url where the visualization is going to be hosted. +```json +{"/":{ + "get":{ + "summary":"Get Url", + "description":"Get the URL of visualizer. It initializes visualizer if it is not initialized", + "operationId":"get_url__get", + "responses":{ + "200":{"description":"Successful Response", + "content":{ + "application/json":{"schema":{}} + } + } + } + } + }, +``` + + +#### GET /info +Get the information of the visualizer instance. It provides the information set by the config.py file which contains the basic settings for creating a VISOR instance. +The Settings object is defined as follows: + +```python +class Settings(BaseSettings): + app_name: str = "VISOR Viewer" + default_host: str = "localhost" + default_port: int = 8081 + default_standalone: bool = True + default_client_bundle: str = Path("client_bundle") + default_log_dir: str = str(Path.cwd().joinpath("logs")) + trame_log_dir: str | None = None +``` +The app_name is able to rename the application name of the component for the current execution. +The default_host and default_port make it possible to provide a different host and port of execution. +The default_standalone is whether the VISOR server is going to be hosting the webclient. +The default_client_bundle is where the client bundle has been deployed for the static client code +The default_log_dir provides the path to logs +The default_trame_log_dir provides the path to trame logging. + + +When the VISOR service is started by an external program like uvicorn the service is using these Settings in order to launch VISOR on a specific host, port and use those logs. + + + +```json +{ + "app_name": "VISOR Viewer", + "host": "localhost", + "port": 8081, + "standalone": true, + "file_input_path": null +} +``` + +### POST initialize/ + +This endpoint provides the chance to initialize the defaults for the Trame service this VISOR service will be controlling. + +```Python +class InitProps(BaseModel): + """Properties for initializing the server.""" + host: str = Field(..., description="Host address", example="localhost") + port: int = Field(..., description="Port number", example=8081) +``` + +```json +"/initialize":{"post":{"summary":"Initialize Server","description":"Initialize the server with the given port, host and client distribution path","operationId":"initialize_server_initialize_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitProps"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}} +``` + + +### POST start/ + +This endpoint starts the visualization for the service on the provided url for a single session. +It can provide an input file on start as an option. +Only files are supported on this API since this is a RESTful service based on HTTP API. + +```json +"/start":{ + "post":{ + "summary":"Start Instance", + "description":"Start visualizer instance", + "operationId":"start_instance_start_post", + "requestBody":{ + "content":{ + "application/json":{ + "schema":{ + "$ref":"#/components/schemas/StartProps"} + } + }, + "required":true}, + "responses":{ + "200":{ + "description":"Successful Response", + "content":{"application/json":{"schema":{}}}}, + "422":{ + "description":"Validation Error", + "content":{ + "application/json":{ + "schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}, + +``` + +The parameter model for the start endpoint are the following: + +```Python +class StartProps(BaseModel): + """Properties for starting the visualizer instance.""" + file_path: Optional[str] = Field(None, description="Path to the input file", example="path/to/file.vtk") + metadata: Optional[Metadata] = Field(None, description="Metadata for the visualizer", example={"name": "test_model", "unit": "m"}) + timeout: Optional[int] = Field(0, description="Timeout in seconds") +``` + +### POST /update + +This endpoint updates the input file to the service. Only files are supported to this endpoint as there is currently no efficient serialization mechanism through this RESTful service for any other data formats. + +```json +"/update":{ + "post":{ + "summary":"Update", + "description":"Update the input file of visualizer instance", + "operationId":"update_update_post", + "requestBody":{ + "content":{ + "application/json":{ + "schema":{ + "$ref":"#/components/schemas/UpdateProps"}}}, + "required":true}, + "responses":{ + "200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}, + "422":{ + "description":"Validation Error", + "content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}, +``` + +The parameter model for the update endpoint are the following: +```Python +class UpdateProps(BaseModel): + """Input for updating the visualizer instance.""" + file_path: str = Field(..., description="Path to the new input file", example="path/to/updated_file.vtk") + metadata: Optional[Metadata] = Field(None, description="Metadata for the visualizer", example={"name": "updated_model", "unit": "m"}) +``` + +### POST /add_dataset + +This endpoint adds a new input file to the service. Only files are supported to this endpoint as there is currently no +efficient serialization mechanism through this RESTful service for any other data formats. + +```json +"/add_dataset": { + "post": { + "summary": "Add Dataset", + "operationId": "add_dataset_add_dataset_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProps" + }, + "example": { + "file_path": "path/to/updated_file.vtk", + "metadata": { + "name": "updated_model", + "unit": "m" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } +}, +``` + + +The parameter model for the add_dataset endpoint are the following (same as the update endpoint): +```Python +class UpdateProps(BaseModel): + """Input for updating the visualizer instance.""" + file_path: str = Field(..., description="Path to the new input file", example="path/to/updated_file.vtk") + metadata: Optional[Metadata] = Field(None, description="Metadata for the visualizer", example={"name": "updated_model", "unit": "m"}) +``` + +### GET /list_datasets +This endpoint lists all datasets currently loaded in the visualizer instance. +```json +"/list_datasets": { + "get": { + "summary": "List Datasets", + "operationId": "list_datasets_list_datasets_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } +}, +``` + +### POST /remove_dataset +This endpoint removes a dataset from the visualizer instance. A dataset ID is required to identify which dataset to remove. + +```json +"/remove_dataset": { + "post": { + "summary": "Remove Dataset", + "operationId": "remove_dataset_remove_dataset_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveDatasetProps" + }, + "example": { + "dataset_id": 123456 + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } +}, +``` + +The parameter model for the remove_dataset endpoint are the following: +```Python +class RemoveDatasetProps(BaseModel): + """Input for removing a dataset from the visualizer instance.""" + dataset_id: int = Field(..., description="ID of the dataset to remove", example=12345) +``` + +### GET /{dataset_id}/list_variables +This endpoint lists variables for a specific dataset in the visualizer instance. + +```json + "/{dataset_id}/list_variables": { + "get": { + "summary": "List Variables", + "operationId": "list_variables__dataset_id__list_variables_get", + "parameters": [ + { + "name": "dataset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Dataset Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } +}, +``` + +### POST /{dataset_id}/update_variables +This endpoint updates variables for a specific dataset in the visualizer instance. + +**Limitation:** This feature is currently only supported for VTK datasets that are either vtkPolyData or vtkUnstructuredGrid. +VISOR also supports vtkMultiBlockDataSet and vtkMultiPieceDataset, and we plan to support for variable updates +on parts within these composite datasets in a future release, but +as of 2026/02/03, that is not yet supported. + +```json +"/{dataset_id}/update_variables": { + "post": { + "summary": "Update Variables", + "operationId": "update_variables__dataset_id__update_variables_post", + "parameters": [ + { + "name": "dataset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Dataset Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateVariableProps" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } +}, +``` + +The parameter model for the update_variables endpoint are the following: +```Python +class UpdateVariableInfo(BaseModel): + """Input for updating a variable in the visualizer instance.""" + name: str = Field(..., description="Name of the variable to update", example="temperature") + type: str = Field(..., description="Type of the variable (point/cell)", example="point") + num_components: int = Field(..., description="Number of components", example=1) + data: list[float] = Field(..., description="Data array for the variable", example=[0.0, 1.0, 2.0, 3.0]) + +class UpdateVariableProps(BaseModel): + """Input for updating a variable in the visualizer instance.""" + variables: list[UpdateVariableInfo] = Field(..., description="List of variables to update") +``` + +### POST /stop_visualization + +This endpoint stops the visualization but not the running service. The Trame server is stopped and the websocket connection is killed but the service is still running. + +```json +"/stop_visualization":{ + "post":{ + "summary":"Stop Instance Visualization", + "description":"Stop instance visualization", + "operationId":"stop_instance_stop_visualization_post", + "responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}} +``` + +### POST /stop + +This endpoint stops the visualization and deletes the visualizer instance. The Trame server is stopped, the websocket connection is killed, and the visualizer instance is cleaned up. + +```json +"/stop":{ + "post":{ + "summary":"Stop Instance", + "description":"Stop visualizer instance", + "operationId":"stop_instance_stop_post", + "responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}} +``` + +### GET /health + +This endpoint provides a basic health check for the RESTful service. + +```json +"/health":{ + "get":{ + "summary":"Health", + "description":"Get the health status of the RESTful service", + "operationId":"health_health_get", + "responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}} +``` + + +### Implementation + +The Python API used for implementing this service is private to the VISOR service and it can be used to create wrappers or other services but it is only useful for this use case. \ No newline at end of file diff --git a/doc/developer_docs/adrs/11-visor-client.md b/doc/developer_docs/adrs/11-visor-client.md new file mode 100644 index 00000000..af6b64af --- /dev/null +++ b/doc/developer_docs/adrs/11-visor-client.md @@ -0,0 +1,197 @@ +# ADR 11: Introduce Python Client and Service Management for VISOR API + +## Status +Proposed + +This ADR was discussed but not adopted as of Aug 28, 2025. + + +## Base Context +Previously, Python users interacted directly with the `Visor` class, using its start, update, and stop methods to +manage visualizations. However, this approach poses challenges in interactive environments like Jupyter notebooks. + +Additionally, we have a command-line interface (CLI) tool, `visor-cli`, which allows users to manage VISOR instances +and visualizations via terminal commands. This CLI interacts with the VISOR HTTP service endpoints, providing +a consistent experience across different interfaces. + +To improve compatibility and usability, we propose updating the Python entrypoint. We introduce a new +`Visor` class that interacts with the VISOR HTTP service instead of instantiating Python-native visualization +object. This new class relies on a lightweight client that wraps HTTP API calls to the +service endpoints and simple service layer that optionally manages the server process. + +The new entrypoint enables reliable and Python-native interaction in notebooks and other interactive environments. +However, it does not support in-memory data inputs, as all interactions occur through the HTTP API. +In this ADR, we outline the proposed changes, their rationale, pros and cons of this approach, and example usage. + + + +## Problem Statement +We expect SAF users to interact with the VISOR using SAF's +[Product Instance Manager](https://saf.glow.docs.solutions.ansys.com/version/stable/user_guide/using_ansys_products/product_instance_management/custom_instance_managers.html) +(PIM) framework. +However, we also want to enable PyAnsys users outside of SAF to interact with VISOR endpoints +in a pythonic way, without requiring the full PIM stack. + +Until now, the VISOR class has served as the main entrypoint for Python users, allowing control +of the Trame server visualization. However, there are some potential limitations of this approach. +1. **Instability in interactive environments**: +Running a Trame server in a background thread within a native Python instance can lead to instability due +to differences in how various environments like scripts, Jupyter notebooks, and interactive shells manage event loops, +I/O, and concurrency. For more reliable and consistent behaviour across contexts, it's often preferable to +isolate the server lifecycle in a dedicated service or process. +2. **Fragmented entrypoints:** +This model introduces two separate entrypoints for VISOR usage: the VISOR HTTP service (via PIM) for SAF users, and +native Python instances for PyAnsys users. While maintaining both paths may offer short-term flexibility for +beta testing and feedback, it may increase long-term development and maintenance overhead. + +## Proposed Solution +To address these limitations, and to allow non-SAF users the ability to interact with the VISOR endpoints +pythonically, we are introducing a new Python `Visor` class that interacts with the HTTP service through +a simple client and service management layer. + +By allowing users to start and stop the VISOR HTTP service from Python in a subprocess, this approach +avoids event loop conflicts in Jupyter notebooks. Running the service in a separate process from the main +notebook allows it to freely perform asynchronous +operations - such as launching, starting, stopping, or updating Trame servers - without interfering +with the notebookโ€™s event loop. + +The `Visor` class manages the HTTP service lifecycle through `uvicorn` in a subprocess, +and it provides programmatic access to the VISOR API endpoints. The user can optionally disable the service +management if they want to run the service themselves or connect to an existing service. + +With these changes, both SAF and non-SAF Python users can interact with VISOR endpoints +without relying on the full PIM stack. The HTTP service can also be run independently, +supporting access from any HTTP client, including Python scripts and web browsers. + + +The proposed changes are as follows: + +1. **New class (User-Facing):** `Visor` + - New main entrypoint for Python users, importable from `ansys.visor.viewer` + - Wrapper around the FastAPI layer (VisorAPI) that would enable a user to interact with the VISOR API + - Automatically runs the VISOR HTTP service via `uvicorn` subprocess, for users who should not need to worry about +starting/stopping the service. + - Includes a `manage_server` parameter to optionally disable automatic server management. +2. Rename old `Visor` class โ†’ `VisorVisualizer` + - Was previously exposed under `ansys.visor.viewer` -> remove this exposure + - Rename old `VisorTrameInterface` class โ†’ `VisorTrameVisualizer` accordingly +(it implements the old `Visor` class). +4. Update `visor-cli` commands to align with the above changes +5. Add a Jupyter notebook to show a concrete example of usage in Python + + +### Summary Table + +See the following table to better summarize how the VISOR HTTP service functionality maps between the HTTP endpoints, Python API, and VISOR CLI. + +| Function | VISOR HTTP Service | Python API | VISOR CLI | +|---------------------------------------------|----------------------------|------------------------------------|---------------------------------| +| **Server Operations** | | | | +| Start server | `uvicorn ...` | `server.start()` | `visor-cli server start` | +| Stop server | `ctrl+c` | `server.stop()` | `ctrl+c` | +| Server health | `/health` | `client.health()` | `visor-cli server health` | +| **Instance Management & Visualization** | | | | +| Connect to (or initialize new) instance | `/initialize` | `client.connect(host='localhost', port=8082)` | `visor-cli instance connect --port 8082` | +| Info about active instance | `/info` | `client.info()` | `visor-cli instance info` | +| Start visualization | `/start` | `client.start_visualization(my_file1)` | `visor-cli instance start path/to/my/file1.vtm` | +| Update visualization | `/update` | `client.update_visualization(my_file2)` | `visor-cli instance update path/to/my/file2.vtm` | +| Stop visualization | `/stop_visualization` | `client.stop_visualization()` | `visor-cli instance stop` | +| Terminate instance | `/stop` | `client.terminate_instance()` | `visor-cli instance terminate` | + + + +## Pros and Cons: Old vs New Entrypoints + +### Old Entrypoint (old `Visor` class -> renamed to `VisorVisualizer`) +**Pros:** +1. Supports in-memory data inputs: Enables workflows that do not require writing files to disk. +2. Simplicity: No need to manage subprocesses or external services. +3. Direct, low-level control: Advanced users can customize and extend behavior more easily. + +**Cons:** +1. Not robust in interactive environments: Event loop conflicts in Jupyter notebooks and similar environments. +2. No parity with CLI or HTTP API: Functionality and experience differ from other interfaces. +3. Limited scalability: Tightly coupled to the Python process, making containerization and +orchestration harder. + +### New Entrypoint (new `Visor` class) +**Pros:** +1. Robust, environment-agnostic usage: Works reliably in Python shells, Jupyter notebooks, CLI, and PIM. +2. Decoupled, language-agnostic architecture: Enables integration with other tools and languages via HTTP API. +3. Improved reliability and maintainability: Isolating the service in a subprocess reduces risk of main process crashes or memory leaks. +4. Unified and simplified instance management: Single entrypoint streamlines support, scaling, and deployment. + +**Cons:** +1. Loss of in-memory input support: All data must be file-based. +2. Increased complexity and resource overhead: Requires managing a subprocess and additional system resources. +3. Error handling complexity: New failure modes (e.g., subprocess management, port conflicts, orphaned processes). +4. Reduced extensibility for advanced users: Some customizations possible with direct in-process access are not feasible +through the HTTP API layer. + + +### In-Memory Data Support +VISOR has a requirement to support in-memory VTK inputs in addition to files. +This is not required for our MVP, but future use cases will require the ability to pass data directly, +without relying on file I/O. + +The `VisorVisualizer` class (the old `Visor` class) still accepts in-memory data inputs, but will no longer +be exposed to Python users. The new `Visor` class does not support in-memory data inputs, as all interactions +occur through the HTTP API. +This is a trade-off to enable robust usage in interactive environments like Jupyter notebooks. + +In order for this new approach to satisfy the in-memory input requirement, we will need to consider how we can enable +this through the HTTP API in a future ADR. Details are outside the scope of the present ADR, but +possible approaches include: +1. Extending the HTTP API to accept JSON payloads representing VTK data. +2. GRPC endpoints for streaming data. + +This will need to be addressed in order to fully satisfy all user requirements. + +## Example Usage + +### VISOR + +The `Visor` class is usable as follows. + +```python +from ansys.visor.viewer import Visor + +# Instantiate the VISOR class. By default, this starts a uvicorn command to run the VISOR service in a subprocess. +visor = Visor() + +# As soon as the VISOR service is initialized, +# a VISOR instance is created and ready on the default host/port +# (by default this is localhost and 8081), so we do not need to +# run the `initialize` API to get a VISOR instance up and running. + +# Start the visualization +visor.start_visualization("examples/assets/tensors9.vtp") +# Update the visualization +visor.update_visualization( + "tests/files/many_blocks/many_blocks.vtm", + metadata={"name": "many_blocks_asset", "unit": "cm"}, +) + +# Stop the visualization but keep the instance +visor.stop_visualization() +# Stop the visualization and delete the VISOR instance +visor.terminate_instance() + +# Initialize a new VISOR instance on a custom host/port +visor.connect(host="localhost", port=8082) + +# Stop the VISOR service by stopping the process running the uvicorn command +visor.shutdown() +``` + + +## Jupyter notebook example +Included in PR [#450](https://github.com/ansys-internal/theia/pull/450) is a Jupyter notebook, which has code similar to the above. +It shows how a user in Python can instantiate/connect to one or more VISOR instances +on different ports and interact with them. + +![Jupyter Notebook Screenshot 1](../images/11_jupyter_notebook_1.png) +![Jupyter Notebook Screenshot 2](../images/11_jupyter_notebook_2.png) + +## Implementation +An implementation of these proposed changes are in PR [#450](https://github.com/ansys-internal/theia/pull/450). diff --git a/doc/developer_docs/adrs/12-metadata-per-part-support.md b/doc/developer_docs/adrs/12-metadata-per-part-support.md new file mode 100644 index 00000000..6a4bb0d2 --- /dev/null +++ b/doc/developer_docs/adrs/12-metadata-per-part-support.md @@ -0,0 +1,42 @@ +# ADR 12: VISOR Metadata per Part Support + +## Status +Proposed + +## Context + +VISOR currently uses a separate `Metadata` class to store visualization-related attributes (e.g., name, unit), +since not all Ansys flagships can yet embed such data directly in their VTK outputs. +A new requirement introduces per-part opacity control. + +Following discussions with Kitware and input from @ahernsean, it was confirmed that VTK `FieldData` can +reliably persist small per-part JSON +metadata on leaf datasets (e.g., .vtp pieces under a .vtm). +In parallel, work is underway across flagships to define a shared data standard, +including support for embedded visualization metadata. + +## Discussion Summary + +* **Recommendation from review**: +Store per-part opacity directly in VTK `FieldData` on each leaf dataset (as a JSON string), for example `{"opacity": 0.5}` +stored under a `vtkStringArray` named `visor_state` +Reserve the external sidecar/Metadata concept for future session-level state. +This ensures defaults travel with geometry, avoids multiblock writer issues, and aligns with emerging VTK conventions. +* **Current team direction**: +Continue using the existing external `Metadata` class in the near term to support early adoption across flagships +and maintain flexibility before the shared format is finalized and adopted. +The `Metadata` object will continue to store per-part visualization settings (e.g., opacity) keyed by part name. + +## Decision +For this ADR: +* Short term: +Implement per-part opacity via the external Metadata class, using part names for association +* Long term: +Migrate to embedded per-leaf `FieldData`, likely once the flagship-standard VTK schema for +visualization metadata is available and adopted. + +## Notes +* Do not use `vtkInformation` for persistence; it is unsuitable for serialized metadata. +* XML-based VTK formats are the recommended path for preserving any per-leaf state. +* The external `Metadata` mechanism remains supported for early adopters and backward compatibility +until a unified data model is in place. \ No newline at end of file diff --git a/doc/developer_docs/adrs/13-scene-details.md b/doc/developer_docs/adrs/13-scene-details.md new file mode 100644 index 00000000..b13369ad --- /dev/null +++ b/doc/developer_docs/adrs/13-scene-details.md @@ -0,0 +1,121 @@ +# ADR 13: Complete VisorSceneDetails Schema + +## Context + +In order for VISOR to properly load the visualizer in a browser, VISOR's Python backend needs to send some data to the VTK-WASM frontend. This is to ensure the object tree, drop-down options, and various labels and such are populated with the correct information. + +In this ADR, we decide on a schema that the frontend will use to send data from the Python backend to the frontend. Specifically, this is the JSON structure that the data will have. The top-level container is called the `VisorSceneDetails`. Children of the `VisorSceneDetails` are the `VisorAppState` and `VtkInfo`. + +## Structure + +```text +VisorSceneDetails +โ”‚ +โ”œโ”€ appState : VisorAppState +โ”‚ โ”‚ +โ”‚ โ”œโ”€ ui : VisorUiState +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ darkTheme : boolean | undefined +โ”‚ โ”‚ โ”œโ”€ panelTopLeftPanelCollapsed : boolean | undefined +โ”‚ โ”‚ โ”œโ”€ panelTopRightPanelCollapsed : boolean | undefined +โ”‚ โ”‚ โ”œโ”€ panelTopRightLegendCollapsed : boolean | undefined +โ”‚ โ”‚ โ””โ”€ panelTopRightTabIndex : number | undefined +โ”‚ โ”‚ +โ”‚ โ””โ”€ scene : VisorSceneState +โ”‚ โ”‚ +โ”‚ โ”œโ”€ unit : string | undefined +โ”‚ โ”œโ”€ orthographicEnabled: boolean | undefined +โ”‚ โ”œโ”€ crossSectionEnabled: boolean | undefined +โ”‚ โ”œโ”€ edgesEnabled: boolean | undefined +โ”‚ โ”œโ”€ boundingBoxEnabled: boolean | undefined +โ”‚ โ”‚ +โ”‚ โ”œโ”€ camera: VisorCameraState +โ”‚ โ”‚ โ”œโ”€ position: number[] | undefined +โ”‚ โ”‚ โ”œโ”€ focalPoint: number[] | undefined +โ”‚ โ”‚ โ”œโ”€ viewUp: number[] | undefined +โ”‚ โ”‚ โ”œโ”€ clippingRange: number[] | undefined +โ”‚ โ”‚ โ”œโ”€ parallelProjection: boolean | undefined +โ”‚ โ”‚ โ”œโ”€ viewAngle: number | undefined +โ”‚ โ”‚ โ””โ”€ parallelScale: number | undefined +โ”‚ โ”‚ +โ”‚ โ”œโ”€ crossSection: VisorCrossSectionState +โ”‚ โ”‚ โ”œโ”€ origin: number[] | undefined +โ”‚ โ”‚ โ””โ”€ normal: number[] | undefined +โ”‚ โ”‚ +โ”‚ โ”œโ”€ datasetStates : Record<string, VisorDatasetState> +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€ [datasetId] : VisorDatasetState +โ”‚ โ”‚ โ”œโ”€ id : string +โ”‚ โ”‚ โ””โ”€ partStates : Record<string, VisorPartState> +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€ [partId] : VisorPartState +โ”‚ โ”‚ โ”œโ”€ id : string +โ”‚ โ”‚ โ”œโ”€ opacity : number | undefined +โ”‚ โ”‚ โ”œโ”€ visible : boolean | undefined +โ”‚ โ”‚ โ”œโ”€ diffuseRgb : number[] | undefined +โ”‚ โ”‚ โ”œโ”€ selected : boolean | undefined +โ”‚ โ”‚ โ”œโ”€ spectrumId : number | null | undefined +โ”‚ โ”‚ โ””โ”€ spectrumComponent : number | undefined +โ”‚ โ”‚ +โ”‚ โ””โ”€ spectrumStates : Record<string, VisorSpectrumState> +โ”‚ โ”‚ +โ”‚ โ””โ”€ [spectrumId] : VisorSpectrumState +โ”‚ โ”œโ”€ id : string +โ”‚ โ”œโ”€ magnitudeRange : [number, number] | undefined +โ”‚ โ””โ”€ ranges : Array<[number, number] | undefined> +โ”‚ +โ””โ”€ vtkInfo : VtkInfo + โ”‚ + โ”œโ”€ orientationWidgetWasmId : number + โ”œโ”€ crossSectionPlaneWasmId : number + โ”œโ”€ crossSectionPlaneWidgetWasmId : number + โ”œโ”€ crossSectionPlaneRepresentationWasmId : number + โ”œโ”€ boundingBoxBoxAlgorithmWasmId : number + โ”œโ”€ boundingBoxOutlineWasmActorId : number + โ”œโ”€ boundingBoxAxesWasmActorId : number + โ”‚ + โ””โ”€ sceneGraph : RootNode + โ”œโ”€ id : number + โ”œโ”€ wasmActorId : number + โ”œโ”€ wasmPropertyId : number + โ”œโ”€ wasmMapperId : number + โ”œโ”€ dataArrays : Array<any> + โ”œโ”€ name : string + โ”œโ”€ isGroupNode : true + โ”œโ”€ isActorNode : false + โ”œโ”€ nodeType : "root" + โ”œโ”€ diffuseColor : [number, number, number] + โ”œโ”€ bounds : number[] + โ”‚ + โ””โ”€ children : Array<DatasetNode> + โ”‚ + โ””โ”€ DatasetNode (vtkMultiBlockDataSet) + โ”œโ”€ id : number + โ”œโ”€ wasmActorId : number + โ”œโ”€ wasmPropertyId : number + โ”œโ”€ wasmMapperId : number + โ”œโ”€ dataArrays : Array<any> + โ”œโ”€ name : string + โ”œโ”€ isGroupNode : true + โ”œโ”€ isActorNode : false + โ”œโ”€ nodeType : "vtkMultiBlockDataSet" + โ”œโ”€ diffuseColor : [number, number, number] + โ”œโ”€ bounds : number[] + โ”‚ + โ””โ”€ children : Array<PartNode> + โ”‚ + โ””โ”€ PartNode (vtkPolyData) + โ”œโ”€ id : number + โ”œโ”€ wasmActorId : number + โ”œโ”€ wasmPropertyId : number + โ”œโ”€ wasmMapperId : number + โ”œโ”€ dataArrays : Array<any> + โ”œโ”€ name : string + โ”œโ”€ isGroupNode : false + โ”œโ”€ isActorNode : true + โ”œโ”€ nodeType : "vtkPolyData" + โ”œโ”€ diffuseColor : [number, number, number] + โ”œโ”€ bounds : number[] + โ””โ”€ children : [] + +``` \ No newline at end of file diff --git a/doc/developer_docs/adrs/14-save-load-state.md b/doc/developer_docs/adrs/14-save-load-state.md new file mode 100644 index 00000000..baaa872d --- /dev/null +++ b/doc/developer_docs/adrs/14-save-load-state.md @@ -0,0 +1,455 @@ +# ADR 14: VISOR Save and Load State +# ================================= + +## Status +Accepted + +## Context +VISOR needs a way for a user to save the current viewer state to disk and restore it later, so they can +close the application and return to the same state when reopening it. While basic Python APIs and method stubs +for saving and loading state exist currently, this functionality is not yet implemented. + +The frontend owns the core visualization state, including UI, navigation controls, widgets, and per-dataset +visualization settings. The backend manages data access and supporting services, but does not maintain a +complete view of the active visualization state on the client. + +Backend-driven changes are sent to the frontend via an existing update mechanism. It provides initial values for +some parts of the state (e.g. dataset per-part opacity), but does not include a complete representation of all +state that would need to be persisted and restored. + +### Decision Summary (high level) +- Persisted format: JSON (versioned) +- Version field name (current code): `version` (e.g. `"1.0"`) +- Persisted keying: + - datasets: by `dataset_name` (string) + - parts: by `part_name` (string) +- Load behavior: best-effort apply; warn+skip mismatches; do not fail the load unless the file is invalid +- Dataset serialization: each dataset is serialized to the save directory in VTKHDF format as + `<dataset_name>_snapshot.vtkhdf` when `save_state` is called + +### Persisted Artifact Contract (v1) +- `save_state(path)` writes a directory containing: + - `visor.json` โ€” the persisted viewer state (JSON, versioned) + - `<dataset_name>_snapshot.vtkhdf` โ€” one VTKHDF file per registered dataset, serialized at save time +- `load_state(path)` reads `visor.json` from that directory and applies it. +- If the scene is empty at load time, `load_state` also loads each dataset from the corresponding + `<dataset_name>_snapshot.vtkhdf` file in the save directory before applying state from `visor.json`. +- If the scene is not empty at load time (one or more datasets already registered), `load_state` skips dataset + loading and applies only the viewer state from `visor.json`. +- The backend owns the persistence contract and validates the JSON; the frontend remains the runtime source-of-truth + for visualization state. + +This ADR is organized as follows: +* **Section 1 (Requirements and Constraints)** defines the scope, requirements, and +assumptions for this feature. +* **Section 2 (VISOR State Models)** clarifies the distinct viewer state representations +involved in this feature, their purpose, ownership, and lifecycle, and how they relate to each other. +* **Section 3 (Rough Proposed State Model)** presents a high-level diagram illustrating the proposed rough state model +structure, and how shared schema blocks are used across different state representations. +* **Section 4 (Client/Server State Synchronization)** describes how save/load interacts with the existing frontend +source-of-truth model and the request/response mechanisms used. +* **Section 5 (Full State Representation)** defines what we intend to capture in the persisted format and what is +phased/deferred. +* **Section 6 (Phased Implementation Plan)** proposes a staged +approach to implementing the feature in a way that manages risk and keeps each step focused. + + + +*** + +## 1. Requirements and Constraints + +### Requirements (functional) +1. Provide service + Python APIs to save and restore viewer state + * save_state(path) + * load_state(path) +2. The saved state should include anything necessary to restore the viewer to the same state, including at least: + * UI state + * dark mode + * open / closed panels + * VTK scene state: + * unit + * camera settings (camera location, point at/from, zooming factor) + * widget states (e.g. enabled, clipping planes, box selection, etc) + * dataset references and per-part settings: + * opacity + * visibility + * selected + * colored by (i.e. active variable) + * variable states + * variable min/max +3. The saved state should include a schema_version field so the format can evolve over time. +4. The load_state API supports loading the dataset inputs with the following behavior: + * If the scene is empty, load_state loads each dataset from the corresponding `<dataset_name>_snapshot.vtkhdf` + file in the save directory, then applies the viewer state from `visor.json`. + * If the scene is not empty (one or more datasets already registered), load_state skips dataset loading and + applies only the viewer state from `visor.json`. +5. The save_state API serializes all registered datasets and viewer state to a directory: + * Each dataset is written as `<dataset_name>_snapshot.vtkhdf` in VTKHDF format. + * The viewer state is written as `visor.json`. + * This is true whether the datasets were originally loaded from disk or constructed from native Python objects. + + **Note:** An `is_dirty` flag is maintained per registered dataset as an implementation-level detail. + The flag is set when a dataset is first registered or subsequently modified, and cleared on a successful + `save_state` call. This allows the implementation to identify datasets that have unsaved changes (i.e. have + been modified or have never been saved). It is a best-effort indicator: it reflects backend-side registrations + and modifications only; changes made solely on the frontend do not affect it. + +#### Definition: "scene is empty" +For the purposes of implementing (4), "scene is empty" means there are no datasets registered on the backend +(i.e., dataset registry count is zero). + + + +### Non-functional requirements +1. Performance: load_state should apply state efficiently and avoid noticeable UI freezes during normal use. +2. Robustness: applying state should tolerate mismatches, applying matches and providing warnings for any mismatches. + +### Out of scope +1. Autosave, crash recovery, or periodic snapshots. +2. Undo/redo support +3. Continuous frontend/backend synchronization (this is meant to be snapshot-based) +4. Exposing the persisted state as a user-editable or programmatically modifiable object outside the save_state/load_state APIs. +5. State file management or registry system (i.e. VISOR does not manage previously saved sessions by ID or otherwise). + +### Assumptions +1. Stable identifiers across sessions are dataset names and part names + * Discussion result 2026/01/09: Yes, use dataset names and part names as stable identifiers. But note that we may have a future +use case for applying the same state to a different dataset with similar structure with similar part names, +but a different dataset name. We can handle this case in the future when it comes up +2. It is the user's responsibility to: + * Provide unique dataset names and unique part names (within a dataset). + * If relying on load_state to load datasets from disk: + * Call the load_state API from an empty scene. + * Ensure the `<dataset_name>_snapshot.vtkhdf` files are present in the directory specified by `load_state` + (these are written automatically by `save_state`). + * If loading datasets manually: + * Load the required datasets before calling load_state. + * Ensure `visor.json` exists in the save directory and corresponds to the intended scene. +3. Mismatch handling: + * If a dataset or part referenced in the saved state does not exist at load time, it is ignored with a warning. + * Any state that can be applied is applied; entries that don't match are skipped. +4. Saves and loads are infrequent/ad hoc. + +### Questions to confirm +1. **State file naming:** should the API accept a file path or a directory path? + * Decision: both `save_state` and `load_state` accept a directory path. `save_state` writes `visor.json` + and one `<dataset_name>_snapshot.vtkhdf` per dataset into that directory. `load_state` reads `visor.json` + from the same directory, and reads dataset snapshots from there if the scene is empty. +2. **Backend-only mechanism:** Is it OK for save/load to be backend-driven only (no frontend button or autosave)? + * Discussion result 2026/01/09: Yes. +3. **Load state error handling:** if load_state reads a valid file, but none of the entries apply (e.g. no matching datasets/parts), should that be treated as an error, or as a successful load with warnings? +4. Will we save and apply the full state, or just the parts that have been changed from defaults? + * Discussion result 2026/01/09: Apply the full state. +5. **Does load_state reset the state to defaults** and then apply the saved state, or just apply the saved state + on top of the current state? + * Discussion result 2026/01/09: Apply on top of current state (additive). + +*** +## 2. VISOR State Models +This feature involves several distinct representations of viewer state that exist for different purposes +and at different points in the application lifecycle. Some of these representations already exist in the codebase +in various forms, while others are clarified here. Although they may contain similar per-part visualization +values, they differ in ownership, structure, mutability, and intended use. + +This section focuses on per-part dataset state, as this is the area where similar data +appears in multiple places at the moment, and has been a source of confusion. + +### Table 1: Per-part dataset state: what exists and why +The table below summarizes the different places where per-part dataset state currently exists in the backend +codebase, along with where each representation lives, what it is used for, and how it is keyed and structured. + +| State Type | Where it lives | What it's for | Key | Shape | +|------------|---------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|-----|--------------------------| +| **Metadata defaults** | `Metadata.state: PersistedDatasetState` | User-defined initial per-part visualization values provided as input (defaults applied at load time) | Per-part name (str) | Flat, single dataset | +| **Runtime state** | `RuntimeDatasetState.partStates` | Live per-part visualization state during an active viewing session | Internal part_id (int) | Nested: dataset -> parts | +| **Persisted state** | `SavedViewerStateV1.scene.dataset_states[*].parts: PersistedDatasetState` | Snapshot of per-part state to save and restore a session later | Per-part name (str) | Nested: dataset โ†’ parts | + +Although each of these representations stores per-part visualization values, they intentionally differ in keying, shape, +and purpose: defining initial defaults, supporting live interaction, and capturing a snapshot for save/load. + +**Note:** For the initial implementation, the per-part schema used for metadata defaults (InitialPartsState) and +persisted state (PersistedViewerDatasetState) may be shared to reduce duplication, as both represent serialized per-part +visualization values keyed by part name. Despite this shared schema, the two representations remain distinct +in purpose and lifecycle (defaults at load time vs snapshot for save/load). + +### Table 2: Per-part dataset state: ownership and lifecycle +The table below summarizes the per-part dataset state representations, including where each representation lives, what +it is used for, and how it is keyed and structured. + +| State Type | Created by / when | Who is allowed to create/edit | Written to disk? | Lifetime | Changes during session? | +|--------------------------------------------------------------|----------------------------------------------------------------------------------------|-------------------------------|---------------------------------------------------|----------|-------------------------| +| **Metadata defaults** | User-authored ahead of time; loaded at dataset import | User (in advance): treated as read-only at runtime | Yes (as input metadata, before any VISOR session) | Exists independently of sessions; loaded as input | No | +| **Frontend runtime per-part state (UI-owned)** | Initialized from backend-provided defaults/load_state results | Frontend during live interaction | No | Lives while dataset is loaded in VISOR session | Yes | +| **Backend runtime per-part state (RuntimePartsState.parts)** | Initialized during dataset/scene setup; refreshed from frontend snapshot when needed (e.g. save) | Backend (via backend operations & applying frontend snapshot) | No | Lives while dataset is loaded in VISOR session | Yes | +| **Persisted state** | Captured at save time and applied at load time | Backend, only via the save/load mechanism | Yes (only via the save/load mechanism) | Exists across sessions; in memory only during save/load | No | + +### Why these representations remain distinct + +These representations look similar in shape, but serve different purposes and cannot be unified without compromising +their individual requirements: + +- **Metadata defaults** are user-authored, serialized, and treated as read-only at runtime. +They use part names for stability across imports. +- **Runtime state** is keyed by internal `part_id` for performance during live interaction and must support fast +lookups and updates. +- **Persisted state** is keyed by part name for stability across sessions and structured by dataset for readability +and partial loading. + +Combining these into a single model would force one representation to satisfy conflicting constraints +(e.g., using part names for runtime lookups would hurt performance; using `part_id` in persisted state would break +across sessions when part IDs change). While the schema for per-part values may be shared where appropriate +(see note in Table 1), the state containers, ownership, and lifecycle remain distinct. + +### Summary + +Together, these representations form a coherent model in which similar schema blocks may be reused, but +state containers, ownership, and lifecycle remain distinct. This separation allows runtime interaction, persistence, +and external-facing defaults to coexist without introducing unintended APIs or tightly coupling +frontend and backend implementations. + +The next section presents a rough proposed state model illustrating how these representations +relate to one another at a high level. + +*** +## 3. Rough Proposed State Model + +The diagram below makes the runtime and persistence structure explicit by showing where per-part dataset state +is expected to exist and how it functions across the system: as user-authored metadata (initial defaults), as +frontend-facing runtime state, and persisted save/load state. + +It also fills in the surrounding runtime state structure to show how these pieces relate to the overall viewer state, +and makes explicit which components may be shared between runtime and persistence, with the expectation that they may +diverge as requirements evolve. + +Note that as this will also require schema changes to the runtime data transfer object (DTO) that is sent from the +backend to the frontend to initialize or update the viewer state. There is a +[separate ADR](https://github.com/ansys-internal/theia/blob/doc/adr-scene-description-1/developer_docs/adrs/13-scene-details.md) to align on this schema. (This ADR will be updated to reflect the final results of that discussion +once it is finalized.) + +![VISOR State Model Diagram](../images/visor-state-model.png) + + +*** +## 4. Client/Server State Synchronization + +During normal client/server interaction in VISOR, the frontend is the source of truth for visualization state, +but the backend may trigger updates via `local_view.update()` in response to backend-driven operations +(e.g., dataset addition). For most such operations driven by server API calls, +the `local_view.update()` trigger causes a full UI rebuild on the client, including state application. + +For the save/load state feature, we expect the following behaviour: at save time, the backend explicitly requests a +snapshot from the frontend to capture the authoritative current state. At load time, the backend reads the saved state +from disk and triggers a frontend update, which will overwrite existing values in the frontend's cached state. + +As part of this feature, we implement the following mechanisms to support backend-driven state updates and requests: + + +* **Load State update mechanism (backend -> frontend state update)** + + * The backend triggers a `setState` call on the client, with the saved state as the payload. + This is fire-and-forget from the server's perspective. + * **Notes:** As the feature expands, we will need to expand the shared schema to fully represent the viewer state + as required for save/load. + +* **Save State request mechanism: backend request for frontend state snapshot** + + * The backend triggers a `getState` request to the client, which is a request for the + frontend to send the current state back to the backend asynchronously. + * The client, in turn, triggers a `save_state_response` function on the server which sends the response + payload to the backend. + + +## 5. Full State Representation + +At the time the save/load state feature was started, the state representation for save/load state was limited +to the per-part dataset state (opacity only). + +For the runtime state at the client/server boundary, we maintain the schema definition in a +[separate ADR](https://github.com/ansys-internal/theia/blob/doc/adr-scene-description-1/developer_docs/adrs/13-scene-details.md). +This schema is intended to evolve as we expand the state representation to include all components required to +capture the viewer state for the save/load feature. + +### 5a. Variable state +Currently, the frontend handles aggregating the global variables in a VISOR scene, which makes it the owner of the +min/max values for each variable, across the scene. +The backend is not currently aware of these global variable states, but they are needed to capture in the save/load +state to ensure a consistent restored state. +As such, we will need to add these variable states to the backend state representation and on-disk format as part of +this feature, and ensure they are included in the frontend snapshot and applied at load time. + +**Note 1:** While the frontend currently manages the global variable aggregation across datasets for the scene, this is something +that would be more appropriate for the backend to manage and communicate to the frontend as part of the variable state. + +This is something we can consider evolving in the future, but for the initial implementation, +we will keep the frontend as the owner of the variable state, and simply ensure it is included in the snapshot and +load application logic for save/load state. + +**Note 2:** The active variable for each part is stored as a field of the per-part dataset state, which is discussed in +5b below. + +**Note 3:** The variable state is required for the first phase of our implementation (as described in section 6 below). + +### 5b. Full per-part dataset state +The per-part dataset state is currently limited to opacity only, but for a complete save/load state, we will need to +expand this to include all relevant per-part visualization values, including: +* visibility +* selected state +* variable colored by (i.e. active variable) +* variable component colored by (i.e. active variable component) + +These values are owned by the client and currently exist in the frontend runtime state, but will need to be included in +the backend state representation and on-disk format for save/load state, and included in the frontend snapshot and +load application logic to ensure a consistent restored state. + +### 5c. UI state +The details of the UI state representation are still to be defined, but will include window collapse/expand state, in +addition to any other state required to ensure a consistent restored state. + +This is currently managed entirely on the frontend, but will need to be included in the save/load state representation +and application logic. + +**Note:** Our first phase (see section 6 below) requires only the UI panel state to be defined and implemented. +The rest of the UI state will be deferred until the second phase, to keep the scope of the first phase +manageable and focused on establishing the core save/load mechanism end to end. + +### 5d. Camera and view state +The details of the camera and view state representation are still to be defined, but includes the camera position, +point at/from, zoom level, focal point, etc. + +This is currently managed entirely on the frontend, but will need to be included in the save/load state representation +and application logic to ensure a consistent restored state. + +### 5d. Widget state +The widget state includes the enabled/disabled state of each widget, as well as any relevant settings for each widget +(e.g. clipping plane positions, box selection bounds, etc). +This is currently managed entirely on the frontend, but will need to be included in the save/load state representation +and application logic to ensure a consistent restored state. + +All widget state is deferred until the second phase of implementation to keep the scope of the first phase manageable +and focused on establishing the core save/load mechanism end to end, with the more complex widget state deferred until +we have that core mechanism in place. + +*** +## 6. Proposed Implementation Plan + +We propose implementing save/load state in two main phases. The goal is to get a working end-to-end solution in place early, +with basic save/load capability, and then expand what is included in a second phase, to more completely capture +the viewer state. + +The first phase will include all save/load state requirements _except_ the widget states and any UI state beyond +dark mode and open/closed panels. The second phase will add widget states and any remaining UI state. + +The backend is the entry point for save and load operations, but the frontend is the source of truth for the +visualization state. The backend needs to explicitly request the current client state at save time. The frontend +will need to ensure the frontend-cached state is correctly overwritten at load time. + +**Summary:** +* Phase 1: End to end save/load state with limited scope, no dataset serialization/loading +* Phase 2: End to end save/load state with limited scope, with dataset serialization and loading +* Phase 3: Full save/load state with _complete viewer state_, with dataset serialization and loading + +### Phase 1: End to end save/load state with limited scope, no dataset loading +The goal of this phase is to implement the core save/load mechanism end to end, with basic state captured and restored. +This phase is broken into two sub-phases to manage risk and keep each step focused. + +#### Phase 1a: End-to-end opacity-only save/load with frontend hooks + +The goal of this sub-phase is to implement the core save/load mechanism end to end, using only the per-part opacity state +as the captured/restored state. This allows us to validate the overall mechanism, without needing to: +* Define the full state schema upfront. +* Implement complex state application logic in the frontend that does not yet exist there. + +The steps involved are: +* Add backend APIs and HTTP endpoints for `save_state` and `load_state`. +* Define the on-disk state format and include basic versioning. +* Implement saving and loading of the per-part opacity state only. + +This phase establishes the core contract between frontend and backend and ensures that load produces a +consistent restored state in the viewer. + + +#### Phase 1b: Expand scope of captured state to include camera settings and basic UI state + +The goal of this sub-phase is to expand the set of state captured and restored to include: +* camera/view state (position, focal point, view up, zoom) +* basic UI state (dark mode, open/closed panels) +* variable state (active variable, min/max) +* per-part visibility, selected state, and colored by + +This phase builds on the core mechanism established in Phase 1a, and expands the state schema +and the frontend application logic to handle the additional state. Depending on the complexity involved, this +may be done in a single user story, or broken into multiple smaller stories, each focused on a specific state area +(e.g. camera, UI, variable, per-part visibility/selection). + +Out of scope: +* widget states +* other UI state beyond dark mode and open/closed panels + +**Result of Phase 1:** +At the end of Phase 1, we will have a working save/load state mechanism that captures and restores the core viewer state, +including camera, UI, variable, and per-part visibility/selection/colored by state. + +Note that this only supports datasets that were loaded from disk originally. `load_state` will not yet provide support +for loading datasets that were created in-memory at this time. + + +### Phase 2: End to end save/load state with limited scope, _with_ dataset serialization and loading +The goal of this phase is to enhance the `load_state` functionality to optionally load datasets from disk +if the current scene is empty. This allows users to restore the core elements of a session, including datasets, +when no datasets are currently loaded in VISOR. + +**Result of Phase 2:** +The end result of Phase 2 is a save/load state mechanism that captures and restores the core relevant viewer state, +including loading datasets from disk if the scene is empty. This allows the user to save a session with datasets, +and restore it later without needing to manually load the datasets first. (This means for the on-prem use +case, save state will serialize the datasets, and later restore without manually needing to load the datasets first.) + +### Phase 3: Full save/load state with _complete viewer state_, with dataset serialization and loading + +Expand saved state included in save/load state to improve completeness across the application. +The goal of this phase is to build on the core save/load mechanism established in Phase 1, and incrementally +expand the set of state that is captured and restored, to eventually achieve a functionally complete +save/load experience. + +This includes: +* widget states (e.g. enabled, clipping planes, box selection, etc) +* any remaining UI state (e.g. panel visibility, collapsed/expanded state, other user-facing controls) + +All of this state is captured via the frontend snapshot and re-applied during load as an explicit overwrite of the +frontend-cached values. + +As additional state is brought into scope: +* the backend state representation and on-disk format are extended to include the new fields +* the state DTO exchanged between backend and frontend is extended accordingly + +Depending on the complexity involved, we may treat this as a single user story, or break it into multiple smaller stories, +each focused on a specific widget or UI component. + +**Result of Phase 3:** +The end result of Phase 3 is a complete save/load state mechanism that captures and restores all relevant viewer state, +making the saved and loaded sessions functionally and visually equivalent from the user's perspective. +Dataset serialization and loading (from Phase 2) remain in place; this phase completes the state coverage by +adding widget and any remaining UI state. + +*** + +### Example persisted JSON shape (illustrative) +This is a minimal example of the intended persisted shape (not a complete schema): + +```json +{ + "version": "1.0", + "ui": { "darkTheme": false }, + "scene": { + "unit": "m", + "dataset_states": { + "my_dataset": { + "parts": { + "part_a": { "opacity": 0.5 } + } + } + } + } +} +``` diff --git a/doc/developer_docs/adrs/15-ansys-product-support.md b/doc/developer_docs/adrs/15-ansys-product-support.md new file mode 100644 index 00000000..f8aa204b --- /dev/null +++ b/doc/developer_docs/adrs/15-ansys-product-support.md @@ -0,0 +1,158 @@ +# VISOR Format Support of Flagship Simulation Products + +## Status +Team and Stakeholder ฮ‘pproved + + +## Decision +VISOR depends on legacy Ansys flagships to provide VTK format support and on PyAnsys APIs to enable those flagships to be used within Solutions Applications. These workflows improve maintainability and adoption by leveraging PyAnsys, which offers a specialized 3D viewer for examples and lowers the effort required for community users to access needed functionality. Each flagship product is responsible for converting its internal data to the common format. That format is aligned with SimAI requirements and maintained by the flagship teams, ensuring it stays optimized and in sync with the 3D viewer as products evolve. + +## Context +Our requirements for legacy Ansys product support were based on the requirements for legacy Ansys products namely, Discovery, SpaceClaim, Fluent, Mechanical, AEDT suite and EnSight. Our prioritized requirements are for Fluent, GeometryService, but all legacy Ansys flagship products are included in the VISOR roadmap as well as Electronics Simulation Products. VISOR is a component which will be available within the Solution Architecture Framework and it is in VISOR's scope to be able to visualize the models and the data produced in those products. It is out of scope for VISOR to be doing transformations of the Ansys product formats to its internal representation. VISOR is using a scene graph described in the [08-scene-graph ADR](https://github.com/ansys-internal/theia/blob/c5ec3e366c8dbe385b5de520ddff10da7eaf6f78/docs/adrs/07-scene-graph.md). This scene description graph supports VTK format. + +VISOR supports VTK-based formats (including VTKHDF), which is the requirement for supporting 3D data along with the mixed OpenUSD and VTK formats from the [architecture board decision #29](https://github.com/ansys-internal/architecture-decision-records/blob/main/content/docs/adrs/0029-visualization-formats.md). Based on that decision, all legacy Ansys products need to support the mixed OpenUSD and VTK format, and for VTK they should be providing VTKHDF. This decision is based on achieving a common visualization format which covers the current needs and the upcoming view regarding OpenUSD format. Considerations for the legacy Ansys common data model may impact some of the decisions here but those discussions will need to make sure to include the requirements and constraints of the VISOR 3D viewer. + +## VISOR currently supports VTK + +VISOR directly supports VTK-based formats. + +_*Advantages*_: + +1. :heavy_check_mark: VISOR viewer as a visualization tool focuses on the visualization capabilities and technology stack which covers its functional and non-functional requirements as a visualization tool. +2. :heavy_check_mark: Avoids overlap with DPF and PyAnsys initiatives which are creating bindings from their formats to open formats such as VTK. +3. :heavy_check_mark: Existing tools such as DPF and PyAnsys bindings can be used to create workflows from Ansys flagship products to VTK format and with the usage of VTKHDF all the information should be encoded to the file format. Only additional information would be necessary for VISOR internal state management. +4. :heavy_check_mark: VISOR is required to enable high-performance workflows, but it is the responsibility of all the different components in the Solutions Application Visualization Workflow to cover the performance requirements of those. This is the reason VISOR is not adding these tools behind any of its APIs. +5. :heavy_check_mark: The developers creating a Solution Application are able to use PyAnsys products to also have post-processing capabilities that a viewer cannot have in its scope. +6. :heavy_check_mark: VISOR's release package and containers contain the minimum dependencies required for VISOR's functional capabilities in order to be aligned with the deployment KPIs of an Shared Technology Component. +7. :heavy_check_mark: VISOR doesn't need to spend development and testing resources for an all-formats-to-one-format. +8. :heavy_check_mark: The usage of open formats for rendering pipelines and formats allows VISOR to have less legacy Ansys proprietary and protected content in terms of visualization which can enable open sourcing part or the whole of the viewer and as such enabling a seamless integration with the PyAnsys initiative and greater adoption. The alignment with the PyAnsys initiative also drives the ability to have maintained and up-to-date APIs since legacy Ansys flagships and the PyAnsys community has put investment on that side which will be continuing in the foreseeable future. + +_*Disadvantages*_: + +1. :x: VISOR is not a single visualization component covering the visualization workflow. The Solution Application Developers need to be able to setup that workflow using PyAnsys APIs or DPF to create a workflow in a Solution. +2. :x: VISOR's input format is not supported by all legacy Ansys Simulation Flagships so the products themselves need to provide that optimized conversion to VTK format. The AVZ workflow has been offering transformations from all Ansys Flagship Simulation Products to its proprietary Ansys Visualization Format (AVZ). In this model, we switch to using VTK, which is an open standard and in that way the proprietary format is only limited to the products and whenever they do updates of their format they will also make sure to maintain their VTK export. + + +### Visualization workflows for Solution Applications + +VISOR is relying on legacy Ansys flagships to provide VTK format support as well as PyAnsys APIs to support flagships in order to be able to be used in Solutions Applications. These workflows allow better maintainability and adoption as PyAnsys has a specialized 3D viewer for examples and showcasing specific products capabilities which is also able to provide less effort for the community users in order to get functionality they are requiring. The flagship products will own the part of converting their internal format to the common format but that format is aligning with SimAI requirements and its also able to be optimized and maintained by them so there is no lag between their updates and the version actually used in the 3D viewer. + + + +### Consequences + +* Deprecation of libraries that were providing code for transforming Discovery and Fluent format to VTK. More specifically the following libraries are being *deprecated*: [visor-geometry-support](https://github.com/ansys-internal/theia-geometry/tree/main/examples) and [visor-fluent-support](https://github.com/ansys-internal/theia-fluent-support). These were created in order to provide proof of concepts and evaluations for VISOR usage but they were never aimed to go to production. +* VISOR will own any specialized formatting it needs which is more specialized than what the generic product support provides. +* PyAnsys initiative pyansys-visualization-tools and any relevant initiatives and PyAnsys APIs for specific products like PyGeometry need to have prioritization in terms of VISOR's technical stakeholders. +* PyAnsys APIs for specific products would benefit from supporting VISOR and having some less performant APIs like we have for PyGeometry until there is full support from products. +* DPF and pyDPF is able to support VISOR as it is and based on the fact that it supports VTKHDF and hierarchical data (assemblies) it can provide more performant bindings if they from their side implement the relevant transformations. + +### Examples and testing + +The [Reference Solution](https://github.com/ansys-internal/airfoil-explorer) created by the Task Force PI&E which targets the Control Plane Blueprint for desktop, on-prem and cloud-native targets is using VISOR for 3D visualization for geometry models created by the GeometryService through PyGeometry APIs. This solution application is also going to be used for testing the integration of these components to provide more long-term support. + + +## Notes + +### 4/7/2026 + +After the [architecture board decision #29](https://github.com/ansys-internal/architecture-decision-records/blob/main/content/docs/adrs/0029-visualization-formats.md) for the common graphics format, a lot of the context of the specific formats for each product is actually obsolete from this discussion as they are required to support the VTK format. The information that was previously outlined can be found here: + +|No | Product| File Extension/ Format | Priority | +----|--------|------------------------|-----------| +| 1 | Fluent |.cas.h5, .dat.h5, msh(.h5), | MVP | +| 2 | SpaceClaim | .scdoc(x) | MVP | +| 3 | Discovery | .dsco | MVP | +| 4 | AEDT | .aedt, .case | High | +| 5 | Mechanical | .cdb, .rst, .rth, .rstp, .rmg | High | +| 6 | Ansys Viewer (AVZ) | .avz | Medium | +| 7 | CFX | .res, .dat, .def | Low | +| 8 | HFSS | .obj, .aedtplt | Low | +| 9 | SIWave | .anf, ODB++, EDB, IPC-2581, DXF, GDSII, .snp | Low| +|10 | Maxwell | .ies, .ldt | Low | +| 11 | EnSight | .case, .encas | Low | + +(*) Note: This prioritization is based on our VISOR board and not on the document which is not currently being updated in terms of priorities but the requirements in terms of formats are still up to date as of 7/31/2025. + + + +## Options and recommended usage per format + +### Fluent + +Fluent formats are the following: .cas.h5, .dat.h5, msh(.h5). There are currently the following options for using VISOR in a Solutions with these formats. + +#### VISORFluentSupport + + +```python +converter = VisorFluentSupport() + +file, metadata = converter.to_vtk_file( + case_file=str(pathlib.Path.joinpath(dir, "input.cas.h5")), + data_file=str(pathlib.Path.joinpath(dir, "input.dat.h5")), + file_path=str(pathlib.Path.joinpath(dir, "output")), +) + +visualization = Visor(input=file, metadata=metadata, standalone=True) +visualization.start() +file, metadata = converter.to_vtk_file( + case_file=str(pathlib.Path.joinpath(dir, "input.cas.h5")), + data_file=str(pathlib.Path.joinpath(dir, "input.dat.h5")), + file_path=str(pathlib.Path.joinpath(dir, "output")), +) +visualization.update(input=file, metadata=metadata) + +visualization.stop() +``` + + +#### DPF/DataBridge + +VISOR will be able to accept files in its Pythonic APIs using DPF APIs as it has been done for DataBridge. If DPF is able to convert to single VtkDataset along with the existing capabilities of DPF to provide a metadata object then the in-memory API can also be used. + + + +### Ansys Geometry Format (SpaceClaim/Discovery/Geometry Service) + +Geometry format from Ansys products Discovery (SpaceClaim) and the Geometry Service support PyVista bindings in their PyAnsys bindings. Using those bindings we can convert to VTK formats and metadata objects and files which are compatible with VISOR. The Solutions Applications Visualization Workflow can use the Geometry Service directly or helper libraries to be able to convert to VTK format. Such a helper library is VisorGeometrySupport +in the following example: + +```python +converter = VisorGeometrySupport() +(model, metadata) = converter.to_vtk_file( + resolve_path("reactor.scdocx"), resolve_path("reactor") +) +visualizer = Visor(input=model, metadata=metadata, standalone=True) +visualizer.start() +``` + +### Ansys Discovery Physics + +Ansys Discovery supports exporting VTK format for geometry and physics but its not currently connected to the Python bindings. This +needs to be a feature request for adding the Python bindings. + +### Ansys Mechanical formats + +A conversion mechanism based on DPF can be used in order to be able to convert these formats to VTK files or datasets and to the metadata object. +This can be done currently and if necessary for ease of use helper libraries could be created as part of the Solutions Applications. + +### EnSight + +EnSight is able to export to VTK format compatible with VISOR and is able to be integrated with VISOR viewer. + +### AVZ + +Conversions from AVZ to VTK can be supported if that is actually proritized from the business cases. + +### Electronics formats + +In order to support formats: .aedt, .case, .res, .dat, .def, obj, .aedtplt, .anf, ODB++, EDB, IPC-2581, DXF, GDSII, .snp, .ies, .ldt we would need to leverage DPF and PyAnsys bindings which are offered from PyAEDT. This work hasn't been prioritized but the basis of capabilities currently exists +from PYAEDT. + + + + + + + diff --git a/doc/developer_docs/adrs/16-visor-saf-integration.md b/doc/developer_docs/adrs/16-visor-saf-integration.md new file mode 100644 index 00000000..e51a9f3b --- /dev/null +++ b/doc/developer_docs/adrs/16-visor-saf-integration.md @@ -0,0 +1,77 @@ +## VISOR SAF Integration + +## Decision +Team approved + +## Context + +VISOR is aiming to be the Solutions' Applications 3D viewer and as such the primary platform that VISOR is going to be used is through Solutions Applications Framework (SAF). The tenets of the VISOR project can be found [here](https://github.com/ansys-internal/theia/blob/054999e0152720954dde2ef2596fc46b8a44133c/docs/adrs/01-theia-tenets.md). Solutions Applications are required to be able to target desktop, on-premise deployment and cloud deployment through integration with SAF, REP and CISL/Cloud Burst platforms. VISOR as a Solutions Applications viewer is scoped to be aiming to the requirements of the Solutions Applications and ACE stakeholders rather than being a standalone application and as such it needs to comply with the requirements the Solutions Applications group, the Architecture Hub and the ACE stakeholders are providing. VISOR is not responsible for Authentication or Authorization of users, or directly deployments or running a service but its responsible for making sure that all the requirements set will be able to be implemented in VISOR through its architecture. + +VISOR is targeting desktop, on-prem and cloud deployments. In terms of MVP, VISOR is targeting desktop deployment and it will iteratively target on-prem and cloud deployments as a component of the Solutions Applications Framework and not as a standalone service. + +VISOR REST service is necessary for Solutions Applications to be able to connect to a running instance of VISOR from independent steps where VISOR is not a subsystem of GLOW. The VISOR service is managed by the SAF Product Instance Manager for its lifecycle and the Product Instance Configuration is expected to be able to be deployed along with SAF on the different deployment targets of the Solutions Applications Framework. + +### Desktop + +#### Requirements + +SAF is requiring that a server running for a session to be running using a [PIM (Product Instance Manager) configuration](https://saf.glow.docs.solutions.ansys.com/version/dev/user_guide/using_ansys_products/product_instance_management/index.html). The PIM configuration requires the following: +* An HTTP service with the following endpoints: +```\```: GET root +```\initialize```: POST initialization +```\start```: POST start +```\stop```: POST stop +```\health```: GET health endpoint + +* There is session management for the service +* PIM implementation of VISOR exposes the rest of the VISOR APIs through a VisorClient +* Stopping the service and it should clean up and remove any temporary directories that were created for the service to be running. +* A VISOR manager is included in [SAF Product Manager](https://github.com/ansys-internal/saf-product-manager/blob/main/src/ansys/saf/product_manager/theia/_theia_manager.py) +* A ``SAFVisorClient`` exposes APIs beyond the interface of the generic SAF Product Manager. As of 1.x version of VISOR it supports ``update`` functionality. +* SAF supported releases of VISOR are included in the SAF Product Configuration package: [saf-product-configuration](https://github.com/ansys-internal/saf-product-configuration/blob/main/src/ansys/saf/product_configuration/theia.py) +* There is an end-to-end test in [SAF Product Manager](https://github.com/ansys-internal/saf-product-manager/blob/main/tests/e2e/test_theia.py) which also tests the ``visordash`` API with the VISOR server configuration for Desktop. + +(*) Note: There is a bug in terms of the PIM Desktop configuration which doesn't allow VM rendering due to using localhost and local ports without API gateway. Issue tracking for these: [glow-engine#622](https://github.com/ansys-internal/glow-engine/issues/622), [saf-product-manager#34](https://github.com/ansys-internal/saf-product-manager/issues/34) + + +### Deployment on-premise and cloud + +VISOR is going to be using HPS for orchestrating on-premise and cloud deployment. VISOR is currently using Trame, which is a client-server architecture for a single session which sets up a web-socket connection based on the public session url of the web socket connection. + +In order for VISOR to support multiple users or sessions it needs to be containerized and deployed through the HPS which will be creating new instances to scale VISOR based on the users or sessions which are necessary for smoothly running the on-premise deployment. + +#### VISOR client-side rendering +Based on the current technology components of VISOR described [here](https://github.com/ansys-internal/theia/blob/054999e0152720954dde2ef2596fc46b8a44133c/docs/adrs/02-theia-technology-components.md) it is using Trame VTK.WASM which is a client based rendering technology on the browser using VTK, Web assembly and OpenGL2.x and when its available WebGPU. This means that performance of VISOR is going to be impacted by network connectivity even though there is a websocket connecting directly to the client, it will still have the relative impact as there are models and data transferred to to the client. + +*Additional requirements:* +* Kubernetes-based containerized version of VISOR +* VISOR running in the same cluster where data for rendering are stored and processed (avoid waiting for loading big data files to a different machine and duplicating those data) +* There is an API Gateway which provides the route to the Trame server instance with a url which is able to be served to the ``visordash`` client running on the browser of the user. Issue tracked here: [glow engine #34](https://github.com/ansys-internal/saf-product-manager/issues/34) + + +#### VISOR server-side rendering + +VISOR will support server-side rendering with the same architecture and the only difference in terms of deployment is the requirement for running ParaView on the server. The ``visordash`` component will be using the same websocket connection client so it will require the publicly available websocket url for the Trame server (visualization server). + +*Additional requirements:* +* Kubernetes-based containerized version of VISOR +* VISOR running in the same cluster where data for rendering are stored and processed (avoid waiting for loading big data files to a different machine and duplicating those data) +* There is an API Gateway which provides the route to the Trame server instance with a url which is able to be served to the ``visordash`` client running on the browser of the user. Issue tracked here: [glow engine #34](https://github.com/ansys-internal/saf-product-manager/issues/34) +* ParaView included in the containerized version of VISOR +* GPU is heavily recommended (as the assumption is that large/complex models are passing through the VTK pipeline) + + +### Notes + +* For the first internal release the stop endpoint is not following OpenTelemetry requirements, it will only report if the VISOR server is healthy. At this point this covers the Trame server but not wslink running or the websocket connection health. + +* A revision of the error codes and the health endpoint to cover OpenTelemetry requirements will be done at a later stage ahead of a full delivery of VISOR. + +*29th of July 2025:* +As of now, an STC is not able to have a Product Instance Manager and Configuration included in GLOW. Only flagships have their configuration supported by the GLOW team. However, the [Product Instance Configuration repo](https://github.com/ansys-internal/saf-product-configuration) is the only that gets deployed along with SAF and has the testing support for PIM. The same goes for the [Product Instance Manager repo](https://github.com/ansys-internal/glow-engine/tree/main/src/ansys/saf/glow/solution/geometry). This repo contains testing which ensures that changes of the PIM configuration do not break the Product Instance Manager. + + +*23rd of December 2025:* +- The first release of VISOR is available and PIM light packages have the mechanisms to support VISOR for Desktop deployment (not containerized and without routing capabilities for external VPC connections) +- On-Premise and Cloud deployments are not yet implemented. The proposal is to use HPS as the orchestrator for VISOR since VISOR is using Trame server which implements a stateful, session based VTK rendering pipeline. +- VISOR is not currently providing a kubernetes based container. It only provides a Docker containerized version. \ No newline at end of file diff --git a/doc/developer_docs/adrs/17-remote-rendering-architecture.md b/doc/developer_docs/adrs/17-remote-rendering-architecture.md new file mode 100644 index 00000000..0d20c530 --- /dev/null +++ b/doc/developer_docs/adrs/17-remote-rendering-architecture.md @@ -0,0 +1,203 @@ +# ADR 17: Server-Authoritative State and Remote Rendering for VISOR + +## Status +Accepted (2026-07-21 by VISOR team) + +## Context +VISOR is a browser-based 3D scientific visualization platform built on Trame, with rendering currently performed +client-side via VTK.wasm (`trame-vtklocal`). This works well for datasets that fit comfortably in browser memory, +but the geometry must be serialized to the client and client hardware bounds performance. + +Remote rendering, in which the server owns the VTK pipeline and rendering and streams rendered frames to the browser, +is a product requirement. The local wasm mode still serves the case where no server GPU is available, and +where the dataset is small enough to fit in browser memory. The Trame framework supports both modes, +with `trame-vtklocal` and `trame-rca` as its packages for the local and remote paths respectively. + +Remote rendering needs the scene to live on the server. In VISOR today it does not: per-part visual state lives +in the browser and is never synced back, so the server's copy is stale by design after startup. Host solutions +drive VISOR through its Python service API, so those calls operate on that stale copy. Wasm-specific code is also +coupled through code on both frontend and backend, leaving no clean seam for a second rendering mode to attach. + +This ADR covers two coupled pieces of work: what the current application needs before a remote path can be added, +and the remote path itself. + +## Requirements +* **Server-authoritative state**: the scene lives on the server; a rendering mode with no client-side scene cannot +depend on client-owned state +* **Server-side rendering**: with pixel streaming to the browser, the existing UI overlaid, and camera and +interaction events forwarded to the server +* **One shared codebase for both modes**, minimizing divergence so that features and fixes apply to both paths +where possible +* **State behaviour identical across both modes**: Save/load state behaves the same regardless of rendering mode +* **API behaviour identical across both modes**: API-driven changes (`add_dataset`, `update_variables`, etc) behave +the same regardless of rendering mode +* **Support for datasets beyond client-side limits**: Rendering mode is selected when the application instance is +created, with the local wasm mode retained for no-server-GPU deployments; switching modes within a running +session is not a requirement. + +## Out of scope + +* Pre-existing issues that are not prerequisites for remote rendering. Performance and threading work is in scope +only where parity requires it. +* Multi-user and multi-tenancy +* In-session rendering mode switching +* Product-level performance targets (this work targets partiy) +* Production hardening of the remote path: session management, reconnect, encoder and quality controls. These +land under the performance epic, not in this ADR. + * A unified graceful auto-connect is deferred to the performance epic. +* Detailed design of the round-trip mechanism. We commit to round trips here, but it is designed separately; +see below. + +## Options Considered + +### Question 1: State ownership and stack + +**Option 1.1 Keep the current hybrid client-owned state model.** Cannot support a rendering mode with no client scene. +Also carries today's known costs: state changes push stale server state on `local_view.update()` before +reapplying state on the client, producing an inherent visible flicker. Not chosen. + +**Option 1.2 Move state fully client-side, off Trame.** A pure JS/VTK.wasm application with a +purpose-built service layer. This maximizes client-side simplicity, but is structurally incompatible with remote +rendering. To support the Python-driven APIs, it means building and owning transport, session, and sync layers +that Trame already provides. Not chosen. + +**Option 1.2 Server-authoritative state within Trame (chosen).** The server owns scene state; Trame remains the session +and delivery layer. This is the only option compatible with remote rendering, it resolves the state reconstruction +limitations noted above independent of rendering mode, and it keeps VISOR on infrastructure maintained upstream. +One cost is that the wasm path's end state requires round trips for state changes, which is the risk the second spike +was run to evaluate (see Spike Summary below). Chosen. + +### Question 2: How the two modes coexist + +**Option 2.1 Single stack with two renderers behind a shared `IRenderer` abstraction (chosen).** Python abstract class and +TypeScript interface; shared code programs against it, each mode implements it. The same React bundle serves both. + +**Option 2.2 Parallel remote path inside the existing app, no shared abstractions.** +The fastest to a first demo, but the current state model and a remote path's state model do not align, so +mode-specific branching spreads through backend and frontend and every feature is effectively built and maintained +twice in one codebase (not clean, difficult to maintain). Not chosen. + +**Option 2.3 Separate frontend for remote mode.** Maximal isolation between the modes, at the cost of two UIs to build +and maintain, a split user experience, and giving up the shared-codebase requirement. Not chosen. + +### Question 3: Remote transport within Trame +The trame-native options are the older image streaming widgets (`VtkRemoteView` / `VtkRemoteLocalView`, trame-vtk) and +`trame-rca` (Remote Controlled Area, Kitware's current purpose-built streaming layer). `trame-rca` was selected as +the current-generation tool, recommended by Kitware. + +### Question 4: Structure of the Trame application layer + +Rendering mode selection at startup is a requirement (see Requirements). The remaining structural question is whether +one Trame application class serves both modes, or each mode has its own. + +**Option 4.1 A single Trame application class serving both modes.** On its face, less total code. In practice the +two modes expose the same trigger contract but complete the triggers differently (apply and sync the WASM scene vs +apply and schedule a server-side render), so a merged class would branch on mode inside nearly every handler. +It also could not deliver in-session mode-switching alone, since a session's mode is decided when its scene, renderer, +and client objects are constructed, and switching is also not a requirement. Not chosen. + +**Option 4.2 One thin Trame application per mode (chosen).** `LocalApp` and `RemoteApp` are separate classes, +each decorated with `@TrameApp`, owning only their mode's web configuration. The frontend trigger contract is +common to both, and the shared Python logic lives in the scene layer beneath them (the scene base and the per-part +pipeline), so the application classes themselves stay thin. A session is unambiguously on one code path from +startup, which is more robust for the SAF integration path. Chosen. + + + +## Decision + +Adopt server-authoritative state within Trame, with dual rendering modes behind an `IRenderer` abstraction. + +* **`IRenderer` on both sides.** Python: scene coordination (dataset registry, state mapping, user-facing API) +no longer owns VTK pipeline objects; a renderer implementation owns the pipeline, render window, and frame delivery. +TypeScript: wasm-specific calls move behind the interface, and an RCA canvas component handles stream display +interaction forwarding in remote mode, with no wasm binary downloaded when running remote. +* **State model.** The server owns the VTK pipeline. Its objects are the authoritative record of scene state. +In local mode the client holds a wasm copy of the scene, so a change originating in the browser has to travel to the +server, be applied to the server's pipeline, and come back as the state the client renders. That round trip is +required by this model. In remote mode the client holds no scene and forwards events. On the wasm path, +local optimizations are preserved where latency demands them (cross-section drag applies locally and syncs on release, +for example). Service API calls, save/load, and refresh all act on the server's objects and behave identically +in both modes. +* **Shared per-part pipeline.** The per-part VTK pipeline (actor, mapper, geometry filter, clipping plane) is identical +in both modes. +* **Remote transport and rendering backends.** `trame-rca` only transports the pixels. The remote path is first built +and de-risked against a VTK off-screen render window with the OpenGL backend (Phase 4 below), which is enough to prove +the architecture. The Paraview `pvserver` is the production backend for large models, and is added as +the last step (Phase 5). +* **Rendering mode selected at startup.** Mode is fixed at instance creation, exposed on the entry point, and plumbed +through the service and CLI. Each mode has its own thin `@TrameApp` class (`LocalApp`, `RemoteApp`) owning only +that mode's web configuration. One unambiguous code path per session is a robust fit for the SAF integration path. + +### Round trips on the wasm path: committed, designed separately + +Committing to server-authoritative state commits us to round trips on the local path; that is a decision confirmed +in this ADR. What that mechanism looks like in detail is a question left out of the scope in this ADR. +Some open design questions include whether the client applies a change optimistically while its round trip is in +flight, whether changes need tracking and acknowledgement and what that would look like, and how camera interaction +behaves on the local path. +The design for the round-trip feature is left for its own ADR under the performance epic (see Implementation Plan +below), informed by the round-trip spike findings. + +## Implementation Plan +Below is the proposed implementation plan, broken into phases. The full user story breakdown is not tracked here. + +``` +Phase 1 (backend refactor)--- + | --> Phase 4 (remote rendering) --> Phase 5 (pveserver) + |--> Phase 3 (state inversion) | + | --> Phase 6 (round trips) + | (under performance epic, separate ADR) +Phase 2 (frontend refactor) -- +``` + +Each user story as part of the implementation plan is expected to leave VISOR in a working state, so that +parallel development work can continue while we incrementally move toward the final architecture. The phases are: + +* Phase 1: backend refactor extracting the renderer abstraction from the scene layer +* Phase 2: frontend refactor extracting wasm-specific code behind the renderer interface (parallel with Phase 1) +* Phase 3: state authority inversion: Incrementally move state from client to server, keeping VISOR functional +at each step. In this phase, the client interactions are still applied locally, but the state is synced back +to the server, and the server's VTK pipeline is updated accordingly. Some parts of the VTK pipeline will need to +be added on the server side. Note that this phase does not yet include the round-trip mechanism itself. +* Phase 4: remote path (RCA canvas, remote renderer, mode selection at VISOR startup). Includes moving geometry +picking server-side with highlights as actors in the server scene, which also adds occlusion. +* Phase 5: Add `pvserver`. Start with a timeboxed spike, followed by an implementation story based on the spike. +* Phase 6 (in parallel after Phase 3): the round trip mechanism. Will be a separate feature under the performance epic. +Requires an ADR for the design of the round-trip mechanism. Due to an upstream issue encountered on the round-trip +spike (see below under Accepted Costs), this phase needs to start with a user story to bump the versions of +`trame-vtklocal` and `vtk-wasm` to the latest versions. + + +## Consequences + +### Positive +* One codebase, two modes, no forked UI or state logic +* Server-authoritative state fixes save/load state and the inherent visible flicker on `local_view.update()` +* Local wasm mode retained for no-server-GPU deployments and small datasets +* Idiomatic to Trame, so maintenance stays aligned with upstream + +### Accepted Costs +* Session-thread affinity is the largest unresolved risk. The spikes surfaced (rather than introduced) a +threading issue. The framework expects VTK operations on the session's thread, and VISOR's API entry points arrive +from the host on other threads. Serializing state applies did not change crash frequency, so the root cause is +open. It affects API correctness after the server-owned model, so it is a parity prerequisite. +* The wasm path runs an interim model after Phase 3 (client-local application, one-way sync back) until the round-trip +feature lands. The end state arrives in two steps, the second under its own ADR. +* An upstream color table issue blocks round trips on the wasm path, with no viable local workaround. The spike branch +investigation found that the issue sits in the framework's state application layer (as opposed to VISOR's code). The +`trame-vtklocal` and `vtk-wasm` dependency bumps at the start of Phase 6 may resolve the issue. If the retest on +the latest versions fails, an upstream ticket would need to be filed to resolve it. (Note that this would leave the wasm +path on the interim model mentioned above - with the remote path being unaffected.) +* `pvserver` introduces a VTK version-pinning consideration (ParaView ships its own VTK), tracked in Phase 5. Pinning +is the initial approach, but we will also explore building a custom `pvserver` against our VTK version. + +## References +* Dual-mode architecture + remote rendering spike user story and spike findings document: + * https://github.com/ansys-internal/theia/issues/1051 +* Round-trip and optimization spike user story: + * https://github.com/ansys-internal/theia/issues/1128 +* User story breakdown for the phased implementation + * https://github.com/ansys-internal/theia/issues/1049 +* User story brekadown for performance epic (round trips, optimizations, and remote rendering production hardening) + * https://github.com/ansys-internal/theia/issues/1168 \ No newline at end of file diff --git a/doc/developer_docs/architecture/.structurizr/images/DashServerComponents-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/DashServerComponents-thumbnail.png new file mode 100644 index 00000000..151649f9 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/DashServerComponents-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/EndUserDeployment-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/EndUserDeployment-thumbnail.png new file mode 100644 index 00000000..b262eed8 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/EndUserDeployment-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/GlowApiServerComponents-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/GlowApiServerComponents-thumbnail.png new file mode 100644 index 00000000..32cbf7ae Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/GlowApiServerComponents-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/GlowContainers-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/GlowContainers-thumbnail.png new file mode 100644 index 00000000..b2855007 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/GlowContainers-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/GlowSystemContext-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/GlowSystemContext-thumbnail.png new file mode 100644 index 00000000..081b3eff Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/GlowSystemContext-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/MethodComponents-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/MethodComponents-thumbnail.png new file mode 100644 index 00000000..abb93591 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/MethodComponents-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/PortalContainers-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/PortalContainers-thumbnail.png new file mode 100644 index 00000000..c275f9ec Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/PortalContainers-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/PortalSystemContext-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/PortalSystemContext-thumbnail.png new file mode 100644 index 00000000..b534bf10 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/PortalSystemContext-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/SystemLandscape-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/SystemLandscape-thumbnail.png new file mode 100644 index 00000000..40bda070 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/SystemLandscape-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/VisorClientComponents-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/VisorClientComponents-thumbnail.png new file mode 100644 index 00000000..bbf67455 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/VisorClientComponents-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/VisorContainers-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/VisorContainers-thumbnail.png new file mode 100644 index 00000000..14aa9d7d Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/VisorContainers-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/VisorServerComponents-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/VisorServerComponents-thumbnail.png new file mode 100644 index 00000000..cd3e78a2 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/VisorServerComponents-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/VisorSolutionApplicationContext-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/VisorSolutionApplicationContext-thumbnail.png new file mode 100644 index 00000000..61844b9f Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/VisorSolutionApplicationContext-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/VisorSystemContext-thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/VisorSystemContext-thumbnail.png new file mode 100644 index 00000000..62b45439 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/VisorSystemContext-thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/images/thumbnail.png b/doc/developer_docs/architecture/.structurizr/images/thumbnail.png new file mode 100644 index 00000000..40bda070 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/images/thumbnail.png differ diff --git a/doc/developer_docs/architecture/.structurizr/index/_0.cfe b/doc/developer_docs/architecture/.structurizr/index/_0.cfe new file mode 100644 index 00000000..d042b2f4 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/index/_0.cfe differ diff --git a/doc/developer_docs/architecture/.structurizr/index/_0.cfs b/doc/developer_docs/architecture/.structurizr/index/_0.cfs new file mode 100644 index 00000000..d70d7828 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/index/_0.cfs differ diff --git a/doc/developer_docs/architecture/.structurizr/index/_0.si b/doc/developer_docs/architecture/.structurizr/index/_0.si new file mode 100644 index 00000000..dbc66db7 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/index/_0.si differ diff --git a/doc/developer_docs/architecture/.structurizr/index/segments_1 b/doc/developer_docs/architecture/.structurizr/index/segments_1 new file mode 100644 index 00000000..423b9170 Binary files /dev/null and b/doc/developer_docs/architecture/.structurizr/index/segments_1 differ diff --git a/doc/developer_docs/architecture/.structurizr/index/write.lock b/doc/developer_docs/architecture/.structurizr/index/write.lock new file mode 100644 index 00000000..e69de29b diff --git a/doc/developer_docs/architecture/visor.md b/doc/developer_docs/architecture/visor.md new file mode 100644 index 00000000..e69de29b diff --git a/doc/developer_docs/architecture/workspace.dsl b/doc/developer_docs/architecture/workspace.dsl new file mode 100644 index 00000000..e6d71f35 --- /dev/null +++ b/doc/developer_docs/architecture/workspace.dsl @@ -0,0 +1,285 @@ +workspace "Visor" "VISOR (Visual Interactive Simulation Object Renderer) 3D Visualization Web Components for Solutions Applications" { + !identifiers hierarchical + !impliedRelationships false + + model { + properties { + "structurizr.groupSeparator" "/" + } + end_user = person "End User" "A person who is using a Solution" "" + group "Ansys Corporate Client" { + + visor = softwareSystem "Visor" "3D Viewer for Solutions Applications" "" { + visor_dash = container "VISOR 3D Viewer Dash UI component" "VISOR 3D Viewer Dash wrapper with Python bindings for the VISOR web client library and client api" "Dash, Python, Typescript, React, VTK WASM JS viewer library" "" { + visor_dash_ui = component "VISOR Viewer web client library" "React TypeScript library of the VISOR client UI" "React,Typescript,Javascript" "#React,#Typescript,#JS" + visor_dash_api = component "VISOR Dash UI Component API" "Provides an API through the React interface available through the Dash component in order to allow for client side callbacks triggering specific functionality from the Dash application" "Dash,React,TypeScript" "#Dash,#JS,#Typescript,#React" + visor_client_api = component "VISOR client UI API" "Implements an API which interfaces and implements actions on the web UI, trame vtk module library and/or the scene component." "React,TypeScript" "#React,#TypeScript" + visor_js_library = component "VISOR JS library" "VISOR JS library implementing the visor client" "JavaScript,TypeScript,React" "#JavaScript,#TypeScript,#React" + } + group "VISOR Client" { + visor_client = container "VISOR JS client" "VISOR client implementing the viewer functionality on the frontend using Trame VTK.WASM module library." "Typescript,JavaScript,React,MJS,Trame,WASM,VTK.WASM" ""{ + visor_scene_component = component "VISOR Viewer Scene Graph component" "VISOR 3D Viewer scene graph component for visualization of the model topology" "VTK, Typescript, React" "#Typescript,#React,#VTK" + visor_ui_elements = component "VISOR UI Elements" "VISOR UI elements for the VISOR viewer" "React,Typescript" "#React,#Typescript" + visor_trame_functionality_api = component "VISOR Trame application sync and state manager" "VISOR API for interfacing with the VISOR defined Trame application on the server" "TypeScript,Trame,React" "" + } + trame_vtk_local_container = container "Trame VTK.WASM library" { + trame_wslink_connection = component "Trame WSLINK connection and WASM loader" "Trame WSLINK connection to the server and WASM loader" "Python, WebSocket, wslink" "#Python,#WebSocket,#wslink" + trame_wasm_handler = component "Trame Object Manager" "VTK Object manager for serializaton/deserialization of VTK C++ classes for VTK pipeline objects shared between the client and the Trame server" "VTK.WASM,JS,MJS" "#VTK,#WASM,#JS,#MJS" + } + } + + visor_server = container "VISOR server" "VISOR server supporting 3D rendering of models from Ansys flagship products" "" "" { + trame_server = component "Trame Server" "Trame server component supporting single session using a web socket connection" "Python, WebSocket, wslink" "#Python,#WebSocket,#wslink" + visor_http_api = component "VISOR Server Orchestration HTTP API" "Provides an HTTP API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection." "OpenAPI,FastAPI" "#openapi,#fastapi" + visor_server_component = component "VISOR Server Component" "VISOR server component creating a Trame server for a single session for this VISOR application" "Python, WebSocket, wslink" "#Python,#WebSocket,#wslink" + } + + visor_app = container "VISOR Application Component" "VISOR Application controlling the choice of rendering engine, a VISOR Server instance and providing the Python API to the application" "Python" ""{ + visor_api = component "VISOR API" "VISOR API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection." "OpenAPI, FastAPI" "#openapi,#fastapi" + trame_application = component "Trame application" "Trame application based on Trame vtk_local application utilizing VTK.WASM and a VTK Object Manager for client-server synchronization" "technology" "tags" + vtk_pipeline = component "VTK Pipeline and shared objects with the client side" "VTK pipeline setup for visualization on the server side which is synchronized with the VTK rendering on the client side" "VTK" "#VTK" + scene_graph = component "Scene graph" "Scene graph for supporting visualization of object hierarchies and scene attributes between the client and the server side" "Python, VTK" "#Python,#VTK" + visor_logmonitor = component "VISOR Logger and Monitor of the application, servers and services" "VISOR logger and monitor is the part of the VISOR application which implements the OpenTelemetry standards for VISOR" "Python,OpenTelemetry" "#Python,#OpenTelemetry" + } + visor_server.visor_http_api -> visor_app.visor_api "Provides an HTTP API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection." "" "#openapi" + visor_app -> visor_server.visor_http_api "Provides an HTTP API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection." "" "#openapi" + + visor_dash.visor_dash_ui -> visor.visor_dash.visor_js_library "Triggers functionality from the Dash client to the VISOR client" "" "#React,#Typescript,#JavaScript" + visor_app.trame_application -> visor_app.vtk_pipeline "Sets up the VTK pipeline for the server side and synchronizes with the client side" + visor_app.trame_application -> visor_app.scene_graph "Sets up the scene graph for the server side" + visor.visor_app.visor_logmonitor -> visor.visor_app.trame_application "Monitors the application and server" + visor.visor_app.visor_api -> visor.visor_app.trame_application "Manages the Trame application client and server side, along with the VTK pipeline, scene management and input management." + visor_app.scene_graph -> visor_client.visor_scene_component "Updates view and sends events to UI elements" + visor.visor_dash.visor_client_api -> visor.visor_client.visor_trame_functionality_api "Triggers functionality from the Dash client to the VISOR client library which is either a web UI functionality, or a Trame VTK.WASM functionality synchronized with the server and/or functionality on the scene component" + visor.visor_dash.visor_dash_api -> visor.visor_client.visor_scene_component "Triggers visualization updates" + visor.visor_dash.visor_dash_api -> visor.visor_client.visor_trame_functionality_api "Triggers functionality from the Dash client to the VISOR client library utilizing Trame VTK.WASM functionality on the client or server side." + visor_server.visor_server_component -> visor_server.trame_server "Lifecycle management of the Trame server" + visor_server.trame_server -> trame_vtk_local_container.trame_wasm_handler "Sends scene updates" + visor.visor_client.visor_trame_functionality_api -> visor_server.trame_server "Trigger VTK updates" + visor.trame_vtk_local_container.trame_wasm_handler -> visor_server.trame_server "Triggers VTK updates" + visor.trame_vtk_local_container.trame_wslink_connection -> visor_server.trame_server "Connects to running wslink session to setup a websocket connection." + visor_server.visor_http_api -> visor_server.visor_server_component "Controls server start, stop and state updates as well as monitoring tasks." + + + end_user -> visor.visor_dash.visor_dash_ui "Triggers 3D model view updates" "" "" + end_user -> visor.visor_dash.visor_dash_api "Triggers 3D model view updates" "" "" + visor.visor_client -> end_user "Visualization of 3D model data" "" "" + visor.visor_client -> end_user "Updates 3D model view" "" "" + visor.visor_client -> visor.visor_server "Requests model data" + visor.visor_server -> visor.visor_client "Sends model data" + } + + portal = softwareSystem "SAF Portal" "enables the user to create new project or select existing project then launch solution UI for project. Does not have responsibility for implementation of any aspect of the solution business logic or the services consumed by the solution." "" { + portal_server = container "Portal Server" "implements a REST API that is consumed by the Portal UI. The portal server consumes a small subset of the API provided by the GLOW API Server" "FastAPI" "#fastapi" + ui = container "Portal User Interface" "provides a view of the projects in the projects directory. enables the user to create or select a project then launch solution UI for the project" "React" + } + + product_instance_manager = softwareSystem "Product Instance Manager" "enables the startup and termination of Ansys Flagship Products or other stateful processes" "" { + url https://tfs.ansys.com:8443/tfs/ANSYS_Development/Extensibility/_git/Root?path=%2Fansys%2Finstancemanagement%2Flight + } + + product = softwareSystem "Ansys Flagship Product" "A stateful process that is required to implement a GLOW transaction method (typically an Ansys Flagship product which contains a simulation solver designed to be a desktop application)" "#external" + + glow = softwareSystem "Guided Low Code Workflow (GLOW)" "framework for vertical applications orientated towards a guided workflow user experience" { + url https://github.com/ansys-internal/glow-engine + dash_ui = container "Solution Dash UI" "A browser based client for the Dash server implemented in React Javascript that renders the UI defined by the Dash server" "React" + + group "API" { + + projects_directory = container "Projects Directory" "the file system directory containing project files" + api = container "API Server" "Provides a REST API specific to a given solution, which is consumed by the solution UI server." + projects_database = container "Projects Database" "stores instances of the solution schema" + + } + + dash = container "Dash Server" "a Flask server that services a React browser based UI defined using the Dash UI definition API" "Flask" "#Flask" { + dash_flask_server = component "Dash Flask Server" "a Flask server that services a React browser based UI defined using the Dash UI definition API" "Flask" { + url https://dash.plotly.com/ + } + solution_ui = component "Solution UI" "a python package which defines how the solution is rendered via the Dash UI definition API" "Python" "" + client_api = component "Client API" "a python package that provides a pythonic interface to a GLOW API server via REST" "Python"{ + url https://github.com/ansys-internal/glow-engine/tree/main/src/ansys/saf/glow/client + } + solution_definition_api = component "GLOW Solution definition API" "a python package that contains the set of python types required to define a GLOW solution" "Python" { + url https://github.com/ansys-internal/glow-engine/tree/main/src/ansys/saf/glow/solution + } + solution = component "Solution definition" "the definition of a solution's schema and business logic" "Python" "" + + solution_ui -> dash_flask_server "invoke rendering providing UI structure and callbacks" "" "#import" + solution_ui -> client_api "gets and sets data; and invokes methods via proxy objects" "" "#function" + client_api -> solution "obtains schema and method set" "" "#import" + solution -> solution_definition_api "obtains base types for solution definition" "" "#import" + client_api -> glow.api "calls" "REST" "#REST" + } + + method_process = container "Method Execution Process" "An OS process that implements a single call to a transaction method" "Python" "" { + method_runner = component "Method Runner" "implements a single call to a transaction method" "Python" { + url https://github.com/ansys-internal/glow-engine/blob/main/src/ansys/saf/glow/_executor/method_runner.py#L41 + } + solution_definition_api = component "Solution definition API" "a python package that contains the set of python types required to define a GLOW solution" "Python"{ + url https://github.com/ansys-internal/glow-engine/tree/main/src/ansys/saf/glow/solution + } + solution = component "Solution definition" "the definition of a solution's schema and business logic" "Python" "" + method_runner -> solution "executes method" "" "#function" + solution -> solution_definition_api "obtains base types for solution definition" "" "#import" + } + + + method_file_space = container "Method file space" "the temporary directory used by a method execution process that exists just for the duration of the process." "file sdystem directory" + product_instance_file_space = container "Product Instance file space" "the OS directory associated with a product instance" "file system directory" "#file" + + api -> method_process "starts & stops" "" "#process" + method_process -> product "executes method code" "gRPC" "#gRPC" + method_process -> product_instance_manager "queries product connection, requests product start & termination" "gRPC" "#gRPC" + method_process -> api "calls" "REST" "#REST" + method_process -> method_file_space "creates & deletes" "" "#file" + method_process -> product_instance_file_space "creates & deletes" "" "#file" + + api -> product_instance_manager "requests product termination (on shutdown)" "gRPC" "#gRPC" + api -> projects_database "gets, modifies & creates records in" "" "#file" + api -> projects_directory "reads and writes project files" "" "#file" + + product_instance_manager -> product "starts & kills" "" "#process" + product_instance_manager -> visor.visor_server "Starts, stops viewer" "" "#process" + visor.visor_server -> method_file_space "reads model data" "" "#file" + visor.visor_server -> product_instance_file_space "reads model data" "" "#file" + product -> method_file_space "writes 3D model" "" "#file" + product -> product_instance_file_space "writes 3D model" "" "#file" + + dash_ui -> dash "obtains code and state; signals user interface events" "REST" "#REST" + dash -> api "Call" "REST" "#REST" + dash_ui -> visor.visor_dash.visor_dash_ui "Signals user interface events" "" "" + dash_ui -> visor.visor_dash.visor_dash_api "Triggers visual events and requests data from the VISOR 3D viewer" "" "" + visor.visor_dash.visor_dash_api -> dash_ui "Sends data to the Dash UI and status responses based on user interaction" + + portal -> api "Call" "REST" "#REST" + + method_process -> api "uploads and downloads fields" "REST" "#REST" + method_process -> method_file_space "creates and deletes" "" "#file" + method_process -> product_instance_file_space "creates and deletes" "" "#file" + + api -> method_process "starts" "" "#process" + + end_user -> dash_ui "uses" "" "#user" + + + end_user -> portal "uses" "" "#user" + portal -> glow "launches UI for existing or new project" + glow -> product_instance_manager "requests start and termination of product instances" "gRPC" "#gRPC" + product_instance_manager -> visor "launches VISOR visualization" "" "" + glow -> visor "launches VISOR visualization" "" "" + portal -> glow.dash_ui "links to" "JavaScript Click Handler" "#link" + glow -> product "calls" "gRPC" "#gRPC" + product_writes_state = product -> glow.projects_directory "reads & writes product state" "" "#file" + end_user -> visor "Views and interacts with the 3D model" + } + } + + end_user_windows_pc_ = deploymentEnvironment "End User Windows Desktop PC" { + deploymentNode "Python Interpreter" "the python interpreter that runs the GLOW solution" "Python" "" 1 { + deploymentNode "Orchestrator" "the python module that starts and shutsdown the GLOW solution" "Python" "" 1 { + api_ = containerInstance glow.api "" { + } + dash_ = containerInstance glow.dash "" { + } + method_process_ = containerInstance glow.method_process "" { + } + portal_server_ = containerInstance portal.portal_server "" { + } + product_instance_manager_ = softwareSystemInstance product_instance_manager "" { + } + } + deploymentNode "pywebview" "a python and browser based engine for rendering web UIs as desktop application windows" "Python" "" 1 { + dash_ui_ = containerInstance glow.dash_ui "" { + } + portal_ui_ = containerInstance portal.ui "" { + } + visor_ui_ = containerInstance visor.visor_client "" "" + } + } + deploymentNode "File System" "the file system of a single Windows Desktop PC" "Windows" 1 { + deploymentNode "User Documents Directory" "the Documents directory of the end user" "Windows" 1 { + projects_directory_ = containerInstance glow.projects_directory + product_instance_file_space_ = containerInstance glow.product_instance_file_space + } + deploymentNode "APPDATA Directory" "the APPDATA directory of the end user" "Windows" 1 { + projects_database_ = containerInstance glow.projects_database " + } + } + } + + } + + views { + theme https://raw.githubusercontent.com/RVR06/cornifer-contrib/main/themes/semantic/theme.json + theme https://raw.githubusercontent.com/RVR06/cornifer-contrib/main/themes/heraldry/theme.json + branding { + logo https://raw.githubusercontent.com/RVR06/cornifer-contrib/main/assets/ansys.png + } + + systemLandscape "SystemLandscape" "VISOR integrated in Solution Application using GLOW" { + include * + autolayout lr + } + + systemContext visor "VisorSolutionApplicationContext" "VISOR Solution Application Context" { + include * + include glow + include portal + autolayout lr + } + + container visor "VisorContainers" "VISOR Containers" { + include end_user + include visor.visor_client + include visor.visor_server + } + + systemContext visor "VisorSystemContext" "VISOR Context" { + include * + autolayout lr + } + + + component visor.visor_server "VisorServerComponents" "VISOR Server Components" { + include * + include visor.visor_server.visor_http_api + include visor.visor_server.visor_server_component + include visor.visor_server.trame_server + autolayout lr + } + + component visor.visor_dash "VisorDashComponents" "VISOR Dash UI Components" { + include * + include visor.visor_dash.visor_dash_ui + include visor.visor_dash.visor_dash_api + include visor.visor_dash.visor_client_api + include visor.visor_dash.visor_js_library + autolayout lr + } + + component visor.visor_client "VisorClientComponents" "VISOR Client Component" { + include * + include visor.visor_client.visor_ui_elements + include visor.visor_client.visor_scene_component + include visor.visor_client.visor_trame_functionality_api + include visor.trame_vtk_local_container.trame_wasm_handler + include visor.trame_vtk_local_container.trame_wslink_connection + autolayout lr + } + + component visor.visor_app "VisorAppComponents" "VISOR Application Components" { + include * + include visor.visor_app.visor_api + include visor.visor_app.trame_application + include visor.visor_app.vtk_pipeline + include visor.visor_app.scene_graph + include visor.visor_app.visor_logmonitor + autolayout lr + } + + deployment * end_user_windows_pc_ "EndUserDeployment" "End User Deployment" { + include * + autolayout lr + } + } \ No newline at end of file diff --git a/doc/developer_docs/architecture/workspace.json b/doc/developer_docs/architecture/workspace.json new file mode 100644 index 00000000..961f1874 --- /dev/null +++ b/doc/developer_docs/architecture/workspace.json @@ -0,0 +1,1875 @@ +{ + "id" : 1, + "name" : "Visor", + "description" : "VISOR (Visual Interactive Simulation Object Renderer) 3D Visualization Web Components for Solutions Applications", + "lastModifiedDate" : "2025-05-22T12:28:08Z", + "lastModifiedAgent" : "structurizr-javascript", + "properties" : { + "structurizr.dsl" : "d29ya3NwYWNlICJUaGVpYSIgIlRoZWlhIChUaHJlZS1kaW1lbnNpb25hbCBFbmdpbmVlcmluZyBJbnRlcmFjdGl2ZSBBbmFseXNpcykgM0QgVmlzdWFsaXphdGlvbiBXZWIgQ29tcG9uZW50cyBmb3IgU29sdXRpb25zIEFwcGxpY2F0aW9ucyIgewoJIWlkZW50aWZpZXJzIGhpZXJhcmNoaWNhbAoJIWltcGxpZWRSZWxhdGlvbnNoaXBzIGZhbHNlCgkKCW1vZGVsIHsKCQlwcm9wZXJ0aWVzIHsKCQkJInN0cnVjdHVyaXpyLmdyb3VwU2VwYXJhdG9yIiAiLyIKCQl9CgkJZW5kX3VzZXIgPSBwZXJzb24gIkVuZCBVc2VyIiAiQSBwZXJzb24gd2hvIGlzIHVzaW5nIGEgU29sdXRpb24iICIiCgkJZ3JvdXAgIkFuc3lzIENvcnBvcmF0ZSBDbGllbnQiIHsKCQkJCgkJCXRoZWlhID0gc29mdHdhcmVTeXN0ZW0gIlRoZWlhIiAiM0QgVmlld2VyIGZvciBTb2x1dGlvbnMgQXBwbGljYXRpb25zIiAiIiB7CgkJCQl0aGVpYV9kYXNoID0gY29udGFpbmVyICJUaGVpYSAzRCBWaWV3ZXIgRGFzaCBVSSBjb21wb25lbnQiICJUaGVpYSAzRCBWaWV3ZXIgRGFzaCB3cmFwcGVyIHdpdGggUHl0aG9uIGJpbmRpbmdzIGZvciB0aGUgVGhlaWEgd2ViIGNsaWVudCBsaWJyYXJ5IGFuZCBjbGllbnQgYXBpIiAiRGFzaCwgUHl0aG9uLCBUeXBlc2NyaXB0LCBSZWFjdCwgVlRLIFdBU00gSlMgdmlld2VyIGxpYnJhcnkiICIiIHsKCQkJCQl0aGVpYV9kYXNoX3VpID0gY29tcG9uZW50ICJUaGVpYSBWaWV3ZXIgd2ViIGNsaWVudCBsaWJyYXJ5IiAiUmVhY3QgVHlwZVNjcmlwdCBsaWJyYXJ5IG9mIHRoZSBUaGVpYSBjbGllbnQgVUkiICJSZWFjdCxUeXBlc2NyaXB0LEphdmFzY3JpcHQiICIjUmVhY3QsI1R5cGVzY3JpcHQsI0pTIgoJCQkJCXRoZWlhX2Rhc2hfYXBpID0gY29tcG9uZW50ICJUaGVpYSBEYXNoIFVJIENvbXBvbmVudCBBUEkiICJQcm92aWRlcyBhbiBBUEkgdGhyb3VnaCB0aGUgUmVhY3QgaW50ZXJmYWNlIGF2YWlsYWJsZSB0aHJvdWdoIHRoZSBEYXNoIGNvbXBvbmVudCBpbiBvcmRlciB0byBhbGxvdyBmb3IgY2xpZW50IHNpZGUgY2FsbGJhY2tzIHRyaWdnZXJpbmcgc3BlY2lmaWMgZnVuY3Rpb25hbGl0eSBmcm9tIHRoZSBEYXNoIGFwcGxpY2F0aW9uIiAiRGFzaCxSZWFjdCxUeXBlU2NyaXB0IiAiI0Rhc2gsI0pTLCNUeXBlc2NyaXB0LCNSZWFjdCIKCQkJCQl0aGVpYV9jbGllbnRfYXBpID0gY29tcG9uZW50ICJUaGVpYSBjbGllbnQgVUkgQVBJIiAiSW1wbGVtZW50cyBhbiBBUEkgd2hpY2ggaW50ZXJmYWNlcyBhbmQgaW1wbGVtZW50cyBhY3Rpb25zIG9uIHRoZSB3ZWIgVUksIHRyYW1lIHZ0ayBtb2R1bGUgbGlicmFyeSBhbmQvb3IgdGhlIHNjZW5lIGNvbXBvbmVudC4iICJSZWFjdCxUeXBlU2NyaXB0IiAiI1JlYWN0LCNUeXBlU2NyaXB0IgoJCQkJCXRoZWlhX2pzX2xpYnJhcnkgID0gY29tcG9uZW50ICJUaGVpYSBKUyBsaWJyYXJ5IiAiVGhlaWEgSlMgbGlicmFyeSBpbXBsZW1lbnRpbmcgdGhlIHRoZWlhIGNsaWVudCIgIkphdmFTY3JpcHQsVHlwZVNjcmlwdCxSZWFjdCIgIiNKYXZhU2NyaXB0LCNUeXBlU2NyaXB0LCNSZWFjdCIKCQkJCX0KCQkJCWdyb3VwICJUaGVpYSBDbGllbnQiIHsKCQkJCQl0aGVpYV9jbGllbnQgPSBjb250YWluZXIgIlRoZWlhIEpTIGNsaWVudCIgIlRoZWlhIGNsaWVudCBpbXBsZW1lbnRpbmcgdGhlIHZpZXdlciBmdW5jdGlvbmFsaXR5IG9uIHRoZSBmcm9udGVuZCB1c2luZyBUcmFtZSBWVEsuV0FTTSBtb2R1bGUgbGlicmFyeS4iICJUeXBlc2NyaXB0LEphdmFTY3JpcHQsUmVhY3QsTUpTLFRyYW1lLFdBU00sVlRLLldBU00iICIiewoJCQkJCQl0aGVpYV9zY2VuZV9jb21wb25lbnQgPSAgY29tcG9uZW50ICJUaGVpYSBWaWV3ZXIgU2NlbmUgR3JhcGggY29tcG9uZW50IiAiVGhlaWEgM0QgVmlld2VyIHNjZW5lIGdyYXBoIGNvbXBvbmVudCBmb3IgdmlzdWFsaXphdGlvbiBvZiB0aGUgbW9kZWwgdG9wb2xvZ3kiICJWVEssIFR5cGVzY3JpcHQsIFJlYWN0IiAiI1R5cGVzY3JpcHQsI1JlYWN0LCNWVEsiCgkJCQkJCXRoZWlhX3VpX2VsZW1lbnRzID0gY29tcG9uZW50ICJUaGVpYSBVSSBFbGVtZW50cyIgIlRoZWlhIFVJIGVsZW1lbnRzIGZvciB0aGUgVGhlaWEgdmlld2VyIiAiUmVhY3QsVHlwZXNjcmlwdCIgIiNSZWFjdCwjVHlwZXNjcmlwdCIKCQkJCQkJdGhlaWFfdHJhbWVfZnVuY3Rpb25hbGl0eV9hcGkgID0gY29tcG9uZW50ICJUaGVpYSBUcmFtZSBhcHBsaWNhdGlvbiBzeW5jIGFuZCBzdGF0ZSBtYW5hZ2VyIiAiVGhlaWEgQVBJIGZvciBpbnRlcmZhY2luZyB3aXRoIHRoZSBUaGVpYSBkZWZpbmVkIFRyYW1lIGFwcGxpY2F0aW9uIG9uIHRoZSBzZXJ2ZXIiICJUeXBlU2NyaXB0LFRyYW1lLFJlYWN0IiAiIgoJCQkJCX0KCQkJCQl0cmFtZV92dGtfbG9jYWxfY29udGFpbmVyID0gY29udGFpbmVyICJUcmFtZSBWVEsuV0FTTSBsaWJyYXJ5IiB7CgkJCQkJCXRyYW1lX3dzbGlua19jb25uZWN0aW9uID0gY29tcG9uZW50ICJUcmFtZSBXU0xJTksgY29ubmVjdGlvbiBhbmQgV0FTTSBsb2FkZXIiICJUcmFtZSBXU0xJTksgY29ubmVjdGlvbiB0byB0aGUgc2VydmVyIGFuZCBXQVNNIGxvYWRlciIgIlB5dGhvbiwgV2ViU29ja2V0LCB3c2xpbmsiICIjUHl0aG9uLCNXZWJTb2NrZXQsI3dzbGluayIKCQkJCQkJdHJhbWVfd2FzbV9oYW5kbGVyID0gY29tcG9uZW50ICJUcmFtZSBPYmplY3QgTWFuYWdlciIgIlZUSyBPYmplY3QgbWFuYWdlciBmb3Igc2VyaWFsaXphdG9uL2Rlc2VyaWFsaXphdGlvbiBvZiBWVEsgQysrIGNsYXNzZXMgZm9yIFZUSyBwaXBlbGluZSBvYmplY3RzIHNoYXJlZCBiZXR3ZWVuIHRoZSBjbGllbnQgYW5kIHRoZSBUcmFtZSBzZXJ2ZXIiICJWVEsuV0FTTSxKUyxNSlMiICIjVlRLLCNXQVNNLCNKUywjTUpTIgoJCQkJCX0KCQkJCX0KCQkJCQoJCQkJdGhlaWFfc2VydmVyID0gY29udGFpbmVyICJUaGVpYSBzZXJ2ZXIiICJUaGVpYSBzZXJ2ZXIgc3VwcG9ydGluZyAzRCByZW5kZXJpbmcgb2YgbW9kZWxzIGZyb20gQW5zeXMgZmxhZ3NoaXAgcHJvZHVjdHMiICIiICIiIHsKCQkJCQl0cmFtZV9zZXJ2ZXIgPSBjb21wb25lbnQgIlRyYW1lIFNlcnZlciIgIlRyYW1lIHNlcnZlciBjb21wb25lbnQgc3VwcG9ydGluZyBzaW5nbGUgc2Vzc2lvbiB1c2luZyBhIHdlYiBzb2NrZXQgY29ubmVjdGlvbiIgIlB5dGhvbiwgV2ViU29ja2V0LCB3c2xpbmsiICIjUHl0aG9uLCNXZWJTb2NrZXQsI3dzbGluayIKCQkJCQl0aGVpYV9odHRwX2FwaSA9IGNvbXBvbmVudCAiVGhlaWEgU2VydmVyIE9yY2hlc3RyYXRpb24gSFRUUCBBUEkiICJQcm92aWRlcyBhbiBIVFRQIEFQSSBmb3IgY29udHJvbGxpbmcgdGhlIGxpZmVjeWNsZSBvZiB0aGUgc3RhcnRpbmcsIHN0b3BwaW5nIG9yIHVwZGF0aW5nIHRoZSBUcmFtZSBzZXJ2ZXIgYW5kIHRoZSB3ZWJzb2NrZXQgY29ubmVjdGlvbi4iICJPcGVuQVBJLEZhc3RBUEkiICIjb3BlbmFwaSwjZmFzdGFwaSIKCQkJCQl0aGVpYV9zZXJ2ZXJfY29tcG9uZW50ID0gY29tcG9uZW50ICJUaGVpYSBTZXJ2ZXIgQ29tcG9uZW50IiAiVGhlaWEgc2VydmVyIGNvbXBvbmVudCBjcmVhdGluZyBhIFRyYW1lIHNlcnZlciBmb3IgYSBzaW5nbGUgc2Vzc2lvbiBmb3IgdGhpcyBUaGVpYSBhcHBsaWNhdGlvbiIgIlB5dGhvbiwgV2ViU29ja2V0LCB3c2xpbmsiICIjUHl0aG9uLCNXZWJTb2NrZXQsI3dzbGluayIKCQkJCX0KCQkJCQoJCQkJdGhlaWFfYXBwID0gY29udGFpbmVyICJUaGVpYSBBcHBsaWNhdGlvbiBDb21wb25lbnQiICJUaGVpYSBBcHBsaWNhdGlvbiBjb250cm9sbGluZyB0aGUgY2hvaWNlIG9mIHJlbmRlcmluZyBlbmdpbmUsIGEgVGhlaWEgU2VydmVyIGluc3RhbmNlIGFuZCBwcm92aWRpbmcgdGhlIFB5dGhvbiBBUEkgdG8gdGhlIGFwcGxpY2F0aW9uIiAiUHl0aG9uIiAiInsKCQkJCQl0aGVpYV9hcGkgPSBjb21wb25lbnQgIlRoZWlhIEFQSSIgIlRoZWlhIEFQSSBmb3IgY29udHJvbGxpbmcgdGhlIGxpZmVjeWNsZSBvZiB0aGUgc3RhcnRpbmcsIHN0b3BwaW5nIG9yIHVwZGF0aW5nIHRoZSBUcmFtZSBzZXJ2ZXIgYW5kIHRoZSB3ZWJzb2NrZXQgY29ubmVjdGlvbi4iICJPcGVuQVBJLCBGYXN0QVBJIiAiI29wZW5hcGksI2Zhc3RhcGkiCgkJCQkJdHJhbWVfYXBwbGljYXRpb24gPSBjb21wb25lbnQgIlRyYW1lIGFwcGxpY2F0aW9uIiAiVHJhbWUgYXBwbGljYXRpb24gYmFzZWQgb24gVHJhbWUgdnRrX2xvY2FsIGFwcGxpY2F0aW9uIHV0aWxpemluZyBWVEsuV0FTTSBhbmQgYSBWVEsgT2JqZWN0IE1hbmFnZXIgZm9yIGNsaWVudC1zZXJ2ZXIgc3luY2hyb25pemF0aW9uIiAidGVjaG5vbG9neSIgInRhZ3MiCgkJCQkJdnRrX3BpcGVsaW5lID0gY29tcG9uZW50ICJWVEsgUGlwZWxpbmUgYW5kIHNoYXJlZCBvYmplY3RzIHdpdGggdGhlIGNsaWVudCBzaWRlIiAiVlRLIHBpcGVsaW5lIHNldHVwIGZvciB2aXN1YWxpemF0aW9uIG9uIHRoZSBzZXJ2ZXIgc2lkZSB3aGljaCBpcyBzeW5jaHJvbml6ZWQgd2l0aCB0aGUgVlRLIHJlbmRlcmluZyBvbiB0aGUgY2xpZW50IHNpZGUiICJWVEsiICIjVlRLIgoJCQkJCXNjZW5lX2dyYXBoICA9IGNvbXBvbmVudCAiU2NlbmUgZ3JhcGgiICJTY2VuZSBncmFwaCBmb3Igc3VwcG9ydGluZyB2aXN1bGl6YXRpb24gb2Ygb2JqZWN0IGhpZXJhcmNoaWVzIGFuZCBzY2VuZSBhdHRyaWJ1dGVzIGJldHdlZW4gdGhlIGNsaWVudCBhbmQgdGhlIHNlcnZlciBzaWRlIiAiUHl0aG9uLCBWVEsiICIjUHl0aG9uLCNWVEsiCgkJCQkJdGhlaWFfbG9nbW9uaXRvciA9IGNvbXBvbmVudCAiVGhlaWEgTG9nZ2VyIGFuZCBNb25pdG9yIG9mIHRoZSBhcHBsaWNhdGlvbiwgc2VydmVycyBhbmQgc2VydmljZXMiICJUaGVpYSBsb2dnZXIgYW5kIG1vbml0b3IgaXMgdGhlIHBhcnQgb2YgdGhlIFRoZWlhIGFwcGxpY2F0aW9uIHdoaWNoIGltcGxlbWVudHMgdGhlIE9wZW5UZWxlbWV0cnkgc3RhbmRhcmRzIGZvciBUaGVpYSIgIlB5dGhvbixPcGVuVGVsZW1ldHJ5IiAiI1B5dGhvbiwjT3BlblRlbGVtZXRyeSIKCQkJCX0KCQkJCXRoZWlhX3NlcnZlci50aGVpYV9odHRwX2FwaSAtPiB0aGVpYV9hcHAudGhlaWFfYXBpICJQcm92aWRlcyBhbiBIVFRQIEFQSSBmb3IgY29udHJvbGxpbmcgdGhlIGxpZmVjeWNsZSBvZiB0aGUgc3RhcnRpbmcsIHN0b3BwaW5nIG9yIHVwZGF0aW5nIHRoZSBUcmFtZSBzZXJ2ZXIgYW5kIHRoZSB3ZWJzb2NrZXQgY29ubmVjdGlvbi4iICIiICIjb3BlbmFwaSIKCQkJCXRoZWlhX2FwcCAtPiB0aGVpYV9zZXJ2ZXIudGhlaWFfaHR0cF9hcGkgIlByb3ZpZGVzIGFuIEhUVFAgQVBJIGZvciBjb250cm9sbGluZyB0aGUgbGlmZWN5Y2xlIG9mIHRoZSBzdGFydGluZywgc3RvcHBpbmcgb3IgdXBkYXRpbmcgdGhlIFRyYW1lIHNlcnZlciBhbmQgdGhlIHdlYnNvY2tldCBjb25uZWN0aW9uLiIgIiIgIiNvcGVuYXBpIgoJCQkJCgkJCQl0aGVpYV9kYXNoLnRoZWlhX2Rhc2hfdWkgLT4gdGhlaWEudGhlaWFfZGFzaC50aGVpYV9qc19saWJyYXJ5ICJUcmlnZ2VycyBmdW5jdGlvbmFsaXR5IGZyb20gdGhlIERhc2ggY2xpZW50IHRvIHRoZSBUaGVpYSBjbGllbnQiICIiICIjUmVhY3QsI1R5cGVzY3JpcHQsI0phdmFTY3JpcHQiCgkJCQl0aGVpYV9hcHAudHJhbWVfYXBwbGljYXRpb24gLT4gdGhlaWFfYXBwLnZ0a19waXBlbGluZSAiU2V0cyB1cCB0aGUgVlRLIHBpcGVsaW5lIGZvciB0aGUgc2VydmVyIHNpZGUgYW5kIHN5bmNocm9uaXplcyB3aXRoIHRoZSBjbGllbnQgc2lkZSIKCQkJCXRoZWlhX2FwcC50cmFtZV9hcHBsaWNhdGlvbiAtPiB0aGVpYV9hcHAuc2NlbmVfZ3JhcGggIlNldHMgdXAgdGhlIHNjZW5lIGdyYXBoIGZvciB0aGUgc2VydmVyIHNpZGUiCgkJCQl0aGVpYS50aGVpYV9hcHAudGhlaWFfbG9nbW9uaXRvciAtPiB0aGVpYS50aGVpYV9hcHAudHJhbWVfYXBwbGljYXRpb24gIk1vbml0b3JzIHRoZSBhcHBsaWNhdGlvbiBhbmQgc2VydmVyIgoJCQkJdGhlaWEudGhlaWFfYXBwLnRoZWlhX2FwaSAtPiB0aGVpYS50aGVpYV9hcHAudHJhbWVfYXBwbGljYXRpb24gIk1hbmFnZXMgdGhlIFRyYW1lIGFwcGxpY2F0aW9uIGNsaWVudCBhbmQgc2VydmVyIHNpZGUsIGFsb25nIHdpdGggdGhlIFZUSyBwaXBlbGluZSwgc2NlbmUgbWFuYWdlbWVudCBhbmQgaW5wdXQgbWFuYWdlbWVudC4iCgkJCQl0aGVpYV9hcHAuc2NlbmVfZ3JhcGggLT4gdGhlaWFfY2xpZW50LnRoZWlhX3NjZW5lX2NvbXBvbmVudCAiVXBkYXRlcyB2aWV3IGFuZCBzZW5kcyBldmVudHMgdG8gVUkgZWxlbWVudHMiCgkJCQl0aGVpYS50aGVpYV9kYXNoLnRoZWlhX2NsaWVudF9hcGkgLT4gdGhlaWEudGhlaWFfY2xpZW50LnRoZWlhX3RyYW1lX2Z1bmN0aW9uYWxpdHlfYXBpICJUcmlnZ2VycyBmdW5jdGlvbmFsaXR5IGZyb20gdGhlIERhc2ggY2xpZW50IHRvIHRoZSBUaGVpYSBjbGllbnQgbGlicmFyeSB3aGljaCBpcyBlaXRoZXIgYSB3ZWIgVUkgZnVuY3Rpb25hbGl0eSwgb3IgYSBUcmFtZSBWVEsuV0FTTSBmdW5jdGlvbmFsaXR5IHN5bmNocm9uaXplZCB3aXRoIHRoZSBzZXJ2ZXIgYW5kL29yIGZ1bmN0aW9uYWxpdHkgb24gdGhlIHNjZW5lIGNvbXBvbmVudCIKCQkJCXRoZWlhLnRoZWlhX2Rhc2gudGhlaWFfZGFzaF9hcGkgLT4gdGhlaWEudGhlaWFfY2xpZW50LnRoZWlhX3NjZW5lX2NvbXBvbmVudCAiVHJpZ2dlcnMgdmlzdWFsaXphdGlvbiB1cGRhdGVzIgoJCQkJdGhlaWEudGhlaWFfZGFzaC50aGVpYV9kYXNoX2FwaSAtPiB0aGVpYS50aGVpYV9jbGllbnQudGhlaWFfdHJhbWVfZnVuY3Rpb25hbGl0eV9hcGkgIlRyaWdnZXJzIGZ1bmN0aW9uYWxpdHkgZnJvbSB0aGUgRGFzaCBjbGllbnQgdG8gdGhlIFRoZWlhIGNsaWVudCBsaWJyYXJ5IHV0aWxpemluZyBUcmFtZSBWVEsuV0FTTSBmdW5jdGlvbmFsaXR5IG9uIHRoZSBjbGllbnQgb3Igc2VydmVyIHNpZGUuIgoJCQkJdGhlaWFfc2VydmVyLnRoZWlhX3NlcnZlcl9jb21wb25lbnQgLT4gdGhlaWFfc2VydmVyLnRyYW1lX3NlcnZlciAiTGlmZWN5Y2xlIG1hbmFnZW1lbnQgb2YgdGhlIFRyYW1lIHNlcnZlciIKCQkJCXRoZWlhX3NlcnZlci50cmFtZV9zZXJ2ZXIgLT4gdHJhbWVfdnRrX2xvY2FsX2NvbnRhaW5lci50cmFtZV93YXNtX2hhbmRsZXIgIlNlbmRzIHNjZW5lIHVwZGF0ZXMiCgkJCQl0aGVpYS50aGVpYV9jbGllbnQudGhlaWFfdHJhbWVfZnVuY3Rpb25hbGl0eV9hcGkgLT4gdGhlaWFfc2VydmVyLnRyYW1lX3NlcnZlciAiVHJpZ2dlciBWVEsgdXBkYXRlcyIKCQkJCXRoZWlhLnRyYW1lX3Z0a19sb2NhbF9jb250YWluZXIudHJhbWVfd2FzbV9oYW5kbGVyIC0+IHRoZWlhX3NlcnZlci50cmFtZV9zZXJ2ZXIgIlRyaWdnZXJzIFZUSyB1cGRhdGVzIgoJCQkJdGhlaWEudHJhbWVfdnRrX2xvY2FsX2NvbnRhaW5lci50cmFtZV93c2xpbmtfY29ubmVjdGlvbiAtPiB0aGVpYV9zZXJ2ZXIudHJhbWVfc2VydmVyICJDb25uZWN0cyB0byBydW5uaW5nIHdzbGluayBzZXNzaW9uIHRvIHNldHVwIGEgd2Vic29ja2V0IGNvbm5lY3Rpb24uIgoJCQkJdGhlaWFfc2VydmVyLnRoZWlhX2h0dHBfYXBpIC0+IHRoZWlhX3NlcnZlci50aGVpYV9zZXJ2ZXJfY29tcG9uZW50ICJDb250cm9scyBzZXJ2ZXIgc3RhcnQsIHN0b3AgYW5kIHN0YXRlIHVwZGF0ZXMgYXMgd2VsbCBhcyBtb25pdG9yaW5nIHRhc2tzLiIKCQkJCQoJCQkJCgkJCQllbmRfdXNlciAtPiB0aGVpYS50aGVpYV9kYXNoLnRoZWlhX2Rhc2hfdWkgIlRyaWdnZXJzIDNEIG1vZGVsIHZpZXcgdXBkYXRlcyIgIiIgIiIKCQkJCWVuZF91c2VyIC0+IHRoZWlhLnRoZWlhX2Rhc2gudGhlaWFfZGFzaF9hcGkgIlRyaWdnZXJzIDNEIG1vZGVsIHZpZXcgdXBkYXRlcyIgIiIgIiIKCQkJCXRoZWlhLnRoZWlhX2NsaWVudCAtPiBlbmRfdXNlciAiVmlzdWFsaXphdGlvbiBvZiAzRCBtb2RlbCBkYXRhIiAiIiAiIgoJCQkJdGhlaWEudGhlaWFfY2xpZW50IC0+IGVuZF91c2VyICJVcGRhdGVzIDNEIG1vZGVsIHZpZXciICIiICIiCgkJCQl0aGVpYS50aGVpYV9jbGllbnQgLT4gdGhlaWEudGhlaWFfc2VydmVyICJSZXF1ZXN0cyBtb2RlbCBkYXRhIgoJCQkJdGhlaWEudGhlaWFfc2VydmVyIC0+IHRoZWlhLnRoZWlhX2NsaWVudCAiU2VuZHMgbW9kZWwgZGF0YSIKCQkJfQoJCQkKCQkJcG9ydGFsID0gc29mdHdhcmVTeXN0ZW0gIlNBRiBQb3J0YWwiICJlbmFibGVzIHRoZSB1c2VyIHRvIGNyZWF0ZSBuZXcgcHJvamVjdCBvciBzZWxlY3QgZXhpc3RpbmcgcHJvamVjdCB0aGVuIGxhdW5jaCBzb2x1dGlvbiBVSSBmb3IgcHJvamVjdC4gIERvZXMgbm90IGhhdmUgcmVzcG9uc2liaWxpdHkgZm9yIGltcGxlbWVudGF0aW9uIG9mIGFueSBhc3BlY3Qgb2YgdGhlIHNvbHV0aW9uIGJ1c2luZXNzIGxvZ2ljIG9yIHRoZSBzZXJ2aWNlcyBjb25zdW1lZCBieSB0aGUgc29sdXRpb24uIiAiIiB7CgkJCQlwb3J0YWxfc2VydmVyID0gY29udGFpbmVyICAiUG9ydGFsIFNlcnZlciIgImltcGxlbWVudHMgYSBSRVNUIEFQSSB0aGF0IGlzIGNvbnN1bWVkIGJ5IHRoZSBQb3J0YWwgVUkuICBUaGUgcG9ydGFsIHNlcnZlciBjb25zdW1lcyBhIHNtYWxsIHN1YnNldCBvZiB0aGUgQVBJIHByb3ZpZGVkIGJ5IHRoZSBHTE9XIEFQSSBTZXJ2ZXIiICJGYXN0QVBJIiAiI2Zhc3RhcGkiCgkJCQl1aSA9IGNvbnRhaW5lciAiUG9ydGFsIFVzZXIgSW50ZXJmYWNlIiAicHJvdmlkZXMgYSB2aWV3IG9mIHRoZSBwcm9qZWN0cyBpbiB0aGUgcHJvamVjdHMgZGlyZWN0b3J5LiAgZW5hYmxlcyB0aGUgdXNlciB0byBjcmVhdGUgb3Igc2VsZWN0IGEgcHJvamVjdCB0aGVuIGxhdW5jaCBzb2x1dGlvbiBVSSBmb3IgdGhlIHByb2plY3QiICJSZWFjdCIKCQkJfQoJCQkKCQkJcHJvZHVjdF9pbnN0YW5jZV9tYW5hZ2VyID0gc29mdHdhcmVTeXN0ZW0gIlByb2R1Y3QgSW5zdGFuY2UgTWFuYWdlciIgImVuYWJsZXMgdGhlIHN0YXJ0dXAgYW5kIHRlcm1pbmF0aW9uIG9mIEFuc3lzIEZsYWdzaGlwIFByb2R1Y3RzIG9yIG90aGVyIHN0YXRlZnVsIHByb2Nlc3NlcyIgIiIgewoJCQkJdXJsIGh0dHBzOi8vdGZzLmFuc3lzLmNvbTo4NDQzL3Rmcy9BTlNZU19EZXZlbG9wbWVudC9FeHRlbnNpYmlsaXR5L19naXQvUm9vdD9wYXRoPSUyRmFuc3lzJTJGaW5zdGFuY2VtYW5hZ2VtZW50JTJGbGlnaHQKCQkJfQoJCQkKCQkJcHJvZHVjdCA9IHNvZnR3YXJlU3lzdGVtICJBbnN5cyBGbGFnc2hpcCBQcm9kdWN0IiAiQSBzdGF0ZWZ1bCBwcm9jZXNzIHRoYXQgaXMgcmVxdWlyZWQgdG8gaW1wbGVtZW50IGEgR0xPVyB0cmFuc2FjdGlvbiBtZXRob2QgKHR5cGljYWxseSBhbiBBbnN5cyBGbGFnc2hpcCBwcm9kdWN0IHdoaWNoIGNvbnRhaW5zIGEgc2ltdWxhdGlvbiBzb2x2ZXIgZGVzaWduZWQgdG8gYmUgYSBkZXNrdG9wIGFwcGxpY2F0aW9uKSIgIiNleHRlcm5hbCIKCQkJCgkJCWdsb3cgPSBzb2Z0d2FyZVN5c3RlbSAiR3VpZGVkIExvdyBDb2RlIFdvcmtmbG93IChHTE9XKSIgImZyYW1ld29yayBmb3IgdmVydGljYWwgYXBwbGljYXRpb25zIG9yaWVudGF0ZWQgdG93YXJkcyBhIGd1aWRlZCB3b3JrZmxvdyB1c2VyIGV4cGVyaWVuY2UiICB7CgkJCQl1cmwgaHR0cHM6Ly9naXRodWIuY29tL2Fuc3lzLWludGVybmFsL2dsb3ctZW5naW5lCgkJCQlkYXNoX3VpID0gY29udGFpbmVyICJTb2x1dGlvbiBEYXNoIFVJIiAiQSBicm93c2VyIGJhc2VkIGNsaWVudCBmb3IgdGhlIERhc2ggc2VydmVyIGltcGxlbWVudGVkIGluIFJlYWN0IEphdmFzY3JpcHQgdGhhdCByZW5kZXJzIHRoZSBVSSBkZWZpbmVkIGJ5IHRoZSBEYXNoIHNlcnZlciIgIlJlYWN0IgoJCQkJCgkJCQlncm91cCAiQVBJIiB7CgkJCQkJCgkJCQkJcHJvamVjdHNfZGlyZWN0b3J5ID0gY29udGFpbmVyICJQcm9qZWN0cyBEaXJlY3RvcnkiICJ0aGUgZmlsZSBzeXN0ZW0gZGlyZWN0b3J5IGNvbnRhaW5pbmcgcHJvamVjdCBmaWxlcyIKCQkJCQlhcGkgPSBjb250YWluZXIgIkFQSSBTZXJ2ZXIiICJQcm92aWRlcyBhIFJFU1QgQVBJIHNwZWNpZmljIHRvIGEgZ2l2ZW4gc29sdXRpb24sIHdoaWNoIGlzIGNvbnN1bWVkIGJ5IHRoZSBzb2x1dGlvbiBVSSBzZXJ2ZXIuIgoJCQkJCXByb2plY3RzX2RhdGFiYXNlID0gY29udGFpbmVyICJQcm9qZWN0cyBEYXRhYmFzZSIgInN0b3JlcyBpbnN0YW5jZXMgb2YgdGhlIHNvbHV0aW9uIHNjaGVtYSIKCQkJCQkKCQkJCX0KCQkJCQoJCQkJZGFzaCA9IGNvbnRhaW5lciAiRGFzaCBTZXJ2ZXIiICJhIEZsYXNrIHNlcnZlciB0aGF0IHNlcnZpY2VzIGEgUmVhY3QgYnJvd3NlciBiYXNlZCBVSSBkZWZpbmVkIHVzaW5nIHRoZSBEYXNoIFVJIGRlZmluaXRpb24gQVBJIiAiRmxhc2siICIjRmxhc2siIHsKCQkJCQlkYXNoX2ZsYXNrX3NlcnZlciA9IGNvbXBvbmVudCAiRGFzaCBGbGFzayBTZXJ2ZXIiICJhIEZsYXNrIHNlcnZlciB0aGF0IHNlcnZpY2VzIGEgUmVhY3QgYnJvd3NlciBiYXNlZCBVSSBkZWZpbmVkIHVzaW5nIHRoZSBEYXNoIFVJIGRlZmluaXRpb24gQVBJIiAiRmxhc2siIHsKCQkJCQkJdXJsIGh0dHBzOi8vZGFzaC5wbG90bHkuY29tLwoJCQkJCX0KCQkJCQlzb2x1dGlvbl91aSA9IGNvbXBvbmVudCAiU29sdXRpb24gVUkiICJhIHB5dGhvbiBwYWNrYWdlIHdoaWNoIGRlZmluZXMgaG93IHRoZSBzb2x1dGlvbiBpcyByZW5kZXJlZCB2aWEgdGhlIERhc2ggVUkgZGVmaW5pdGlvbiBBUEkiICJQeXRob24iICIiCgkJCQkJY2xpZW50X2FwaSA9IGNvbXBvbmVudCAiQ2xpZW50IEFQSSIgImEgcHl0aG9uIHBhY2thZ2UgdGhhdCBwcm92aWRlcyBhIHB5dGhvbmljIGludGVyZmFjZSB0byBhIEdMT1cgQVBJIHNlcnZlciB2aWEgUkVTVCIgIlB5dGhvbiJ7CgkJCQkJCXVybCBodHRwczovL2dpdGh1Yi5jb20vYW5zeXMtaW50ZXJuYWwvZ2xvdy1lbmdpbmUvdHJlZS9tYWluL3NyYy9hbnN5cy9zYWYvZ2xvdy9jbGllbnQKCQkJCQl9CgkJCQkJc29sdXRpb25fZGVmaW5pdGlvbl9hcGkgPSBjb21wb25lbnQgIkdMT1cgU29sdXRpb24gZGVmaW5pdGlvbiBBUEkiICJhIHB5dGhvbiBwYWNrYWdlIHRoYXQgY29udGFpbnMgdGhlIHNldCBvZiBweXRob24gdHlwZXMgcmVxdWlyZWQgdG8gZGVmaW5lIGEgR0xPVyBzb2x1dGlvbiIgIlB5dGhvbiIgewoJCQkJCQl1cmwgaHR0cHM6Ly9naXRodWIuY29tL2Fuc3lzLWludGVybmFsL2dsb3ctZW5naW5lL3RyZWUvbWFpbi9zcmMvYW5zeXMvc2FmL2dsb3cvc29sdXRpb24KCQkJCQl9CgkJCQkJc29sdXRpb24gPSBjb21wb25lbnQgIlNvbHV0aW9uIGRlZmluaXRpb24iICJ0aGUgZGVmaW5pdGlvbiBvZiBhIHNvbHV0aW9uJ3Mgc2NoZW1hIGFuZCBidXNpbmVzcyBsb2dpYyIgIlB5dGhvbiIgIiIKCQkJCQkKCQkJCQlzb2x1dGlvbl91aSAtPiBkYXNoX2ZsYXNrX3NlcnZlciAiaW52b2tlIHJlbmRlcmluZyBwcm92aWRpbmcgVUkgc3RydWN0dXJlIGFuZCBjYWxsYmFja3MiICIiICIjaW1wb3J0IgoJCQkJCXNvbHV0aW9uX3VpIC0+IGNsaWVudF9hcGkgImdldHMgYW5kIHNldHMgZGF0YTsgYW5kIGludm9rZXMgbWV0aG9kcyB2aWEgcHJveHkgb2JqZWN0cyIgIiIgIiNmdW5jdGlvbiIKCQkJCQljbGllbnRfYXBpIC0+IHNvbHV0aW9uICJvYnRhaW5zIHNjaGVtYSBhbmQgbWV0aG9kIHNldCIgIiIgIiNpbXBvcnQiCgkJCQkJc29sdXRpb24gLT4gc29sdXRpb25fZGVmaW5pdGlvbl9hcGkgIm9idGFpbnMgYmFzZSB0eXBlcyBmb3Igc29sdXRpb24gZGVmaW5pdGlvbiIgIiIgIiNpbXBvcnQiCgkJCQkJY2xpZW50X2FwaSAtPiBnbG93LmFwaSAiY2FsbHMiICJSRVNUIiAiI1JFU1QiCgkJCQl9CgkJCQkKCQkJCW1ldGhvZF9wcm9jZXNzID0gY29udGFpbmVyICJNZXRob2QgRXhlY3V0aW9uIFByb2Nlc3MiICJBbiBPUyBwcm9jZXNzIHRoYXQgaW1wbGVtZW50cyBhIHNpbmdsZSBjYWxsIHRvIGEgdHJhbnNhY3Rpb24gbWV0aG9kIiAiUHl0aG9uIiAiIiB7CgkJCQkJbWV0aG9kX3J1bm5lciA9IGNvbXBvbmVudCAiTWV0aG9kIFJ1bm5lciIgImltcGxlbWVudHMgYSBzaW5nbGUgY2FsbCB0byBhIHRyYW5zYWN0aW9uIG1ldGhvZCIgIlB5dGhvbiIgewoJCQkJCQl1cmwgaHR0cHM6Ly9naXRodWIuY29tL2Fuc3lzLWludGVybmFsL2dsb3ctZW5naW5lL2Jsb2IvbWFpbi9zcmMvYW5zeXMvc2FmL2dsb3cvX2V4ZWN1dG9yL21ldGhvZF9ydW5uZXIucHkjTDQxCgkJCQkJfQoJCQkJCXNvbHV0aW9uX2RlZmluaXRpb25fYXBpID0gY29tcG9uZW50ICJTb2x1dGlvbiBkZWZpbml0aW9uIEFQSSIgImEgcHl0aG9uIHBhY2thZ2UgdGhhdCBjb250YWlucyB0aGUgc2V0IG9mIHB5dGhvbiB0eXBlcyByZXF1aXJlZCB0byBkZWZpbmUgYSBHTE9XIHNvbHV0aW9uIiAiUHl0aG9uInsKCQkJCQkJdXJsIGh0dHBzOi8vZ2l0aHViLmNvbS9hbnN5cy1pbnRlcm5hbC9nbG93LWVuZ2luZS90cmVlL21haW4vc3JjL2Fuc3lzL3NhZi9nbG93L3NvbHV0aW9uCgkJCQkJfQoJCQkJCXNvbHV0aW9uID0gY29tcG9uZW50ICJTb2x1dGlvbiBkZWZpbml0aW9uIiAidGhlIGRlZmluaXRpb24gb2YgYSBzb2x1dGlvbidzIHNjaGVtYSBhbmQgYnVzaW5lc3MgbG9naWMiICJQeXRob24iICIiCgkJCQkJbWV0aG9kX3J1bm5lciAtPiBzb2x1dGlvbiAiZXhlY3V0ZXMgbWV0aG9kIiAiIiAiI2Z1bmN0aW9uIgoJCQkJCXNvbHV0aW9uIC0+IHNvbHV0aW9uX2RlZmluaXRpb25fYXBpICJvYnRhaW5zIGJhc2UgdHlwZXMgZm9yIHNvbHV0aW9uIGRlZmluaXRpb24iICIiICIjaW1wb3J0IgoJCQkJfQoJCQkJCgkJCQkKCQkJCW1ldGhvZF9maWxlX3NwYWNlID0gY29udGFpbmVyICJNZXRob2QgZmlsZSBzcGFjZSIgInRoZSB0ZW1wb3JhcnkgZGlyZWN0b3J5IHVzZWQgYnkgYSBtZXRob2QgZXhlY3V0aW9uIHByb2Nlc3MgdGhhdCBleGlzdHMganVzdCBmb3IgdGhlIGR1cmF0aW9uIG9mIHRoZSBwcm9jZXNzLiIgImZpbGUgc2R5c3RlbSBkaXJlY3RvcnkiCgkJCQlwcm9kdWN0X2luc3RhbmNlX2ZpbGVfc3BhY2UgPSBjb250YWluZXIgIlByb2R1Y3QgSW5zdGFuY2UgZmlsZSBzcGFjZSIgInRoZSBPUyBkaXJlY3RvcnkgYXNzb2NpYXRlZCB3aXRoIGEgcHJvZHVjdCBpbnN0YW5jZSIgImZpbGUgc3lzdGVtIGRpcmVjdG9yeSIgIiNmaWxlIgoJCQkJCgkJCQlhcGkgLT4gbWV0aG9kX3Byb2Nlc3MgInN0YXJ0cyAmIHN0b3BzIiAiIiAiI3Byb2Nlc3MiCgkJCQltZXRob2RfcHJvY2VzcyAtPiBwcm9kdWN0ICJleGVjdXRlcyBtZXRob2QgY29kZSIgImdSUEMiICIjZ1JQQyIKCQkJCW1ldGhvZF9wcm9jZXNzIC0+IHByb2R1Y3RfaW5zdGFuY2VfbWFuYWdlciAicXVlcmllcyBwcm9kdWN0IGNvbm5lY3Rpb24sIHJlcXVlc3RzIHByb2R1Y3Qgc3RhcnQgJiB0ZXJtaW5hdGlvbiIgImdSUEMiICIjZ1JQQyIKCQkJCW1ldGhvZF9wcm9jZXNzIC0+IGFwaSAiY2FsbHMiICJSRVNUIiAiI1JFU1QiCgkJCQltZXRob2RfcHJvY2VzcyAtPiBtZXRob2RfZmlsZV9zcGFjZSAiY3JlYXRlcyAmIGRlbGV0ZXMiICIiICIjZmlsZSIKCQkJCW1ldGhvZF9wcm9jZXNzIC0+IHByb2R1Y3RfaW5zdGFuY2VfZmlsZV9zcGFjZSAiY3JlYXRlcyAmIGRlbGV0ZXMiICIiICIjZmlsZSIKCQkJCQoJCQkJYXBpIC0+IHByb2R1Y3RfaW5zdGFuY2VfbWFuYWdlciAicmVxdWVzdHMgcHJvZHVjdCB0ZXJtaW5hdGlvbiAob24gc2h1dGRvd24pIiAiZ1JQQyIgIiNnUlBDIgoJCQkJYXBpIC0+IHByb2plY3RzX2RhdGFiYXNlICJnZXRzLCBtb2RpZmllcyAmIGNyZWF0ZXMgcmVjb3JkcyBpbiIgIiIgIiNmaWxlIgoJCQkJYXBpIC0+IHByb2plY3RzX2RpcmVjdG9yeSAicmVhZHMgYW5kIHdyaXRlcyBwcm9qZWN0IGZpbGVzIiAiIiAiI2ZpbGUiCgkJCQkKCQkJCXByb2R1Y3RfaW5zdGFuY2VfbWFuYWdlciAtPiBwcm9kdWN0ICJzdGFydHMgJiBraWxscyIgIiIgIiNwcm9jZXNzIgoJCQkJcHJvZHVjdF9pbnN0YW5jZV9tYW5hZ2VyIC0+IHRoZWlhLnRoZWlhX3NlcnZlciAiU3RhcnRzLCBzdG9wcyB2aWV3ZXIiICIiICIjcHJvY2VzcyIKCQkJCXRoZWlhLnRoZWlhX3NlcnZlciAtPiBtZXRob2RfZmlsZV9zcGFjZSAicmVhZHMgbW9kZWwgZGF0YSIgIiIgIiNmaWxlIgoJCQkJdGhlaWEudGhlaWFfc2VydmVyIC0+IHByb2R1Y3RfaW5zdGFuY2VfZmlsZV9zcGFjZSAicmVhZHMgbW9kZWwgZGF0YSIgIiIgIiNmaWxlIgoJCQkJcHJvZHVjdCAtPiBtZXRob2RfZmlsZV9zcGFjZSAid3JpdGVzIDNEIG1vZGVsIiAiIiAiI2ZpbGUiCgkJCQlwcm9kdWN0IC0+IHByb2R1Y3RfaW5zdGFuY2VfZmlsZV9zcGFjZSAid3JpdGVzIDNEIG1vZGVsIiAiIiAiI2ZpbGUiCgkJCQkKCQkJCWRhc2hfdWkgLT4gZGFzaCAib2J0YWlucyBjb2RlIGFuZCBzdGF0ZTsgc2lnbmFscyB1c2VyIGludGVyZmFjZSBldmVudHMiICJSRVNUIiAiI1JFU1QiCgkJCQlkYXNoIC0+IGFwaSAiQ2FsbCIgIlJFU1QiICIjUkVTVCIKCQkJCWRhc2hfdWkgLT4gdGhlaWEudGhlaWFfZGFzaC50aGVpYV9kYXNoX3VpICJTaWduYWxzIHVzZXIgaW50ZXJmYWNlIGV2ZW50cyIgIiIgIiIKCQkJCWRhc2hfdWkgLT4gdGhlaWEudGhlaWFfZGFzaC50aGVpYV9kYXNoX2FwaSAiVHJpZ2dlcnMgdmlzdWFsIGV2ZW50cyBhbmQgcmVxdWVzdHMgZGF0YSBmcm9tIHRoZSBUaGVpYSAzRCB2aWV3ZXIiICIiICIiCgkJCQl0aGVpYS50aGVpYV9kYXNoLnRoZWlhX2Rhc2hfYXBpIC0+IGRhc2hfdWkgIlNlbmRzIGRhdGEgdG8gdGhlIERhc2ggVUkgYW5kIHN0YXR1cyByZXNwb25zZXMgYmFzZWQgb24gdXNlciBpbnRlcmFjdGlvbiIKCQkJCQoJCQkJcG9ydGFsIC0+IGFwaSAiQ2FsbCIgIlJFU1QiICIjUkVTVCIKCQkJCQoJCQkJbWV0aG9kX3Byb2Nlc3MgLT4gYXBpICJ1cGxvYWRzIGFuZCBkb3dubG9hZHMgZmllbGRzIiAiUkVTVCIgIiNSRVNUIgoJCQkJbWV0aG9kX3Byb2Nlc3MgLT4gbWV0aG9kX2ZpbGVfc3BhY2UgImNyZWF0ZXMgYW5kIGRlbGV0ZXMiICIiICIjZmlsZSIKCQkJCW1ldGhvZF9wcm9jZXNzIC0+IHByb2R1Y3RfaW5zdGFuY2VfZmlsZV9zcGFjZSAiY3JlYXRlcyBhbmQgZGVsZXRlcyIgIiIgIiNmaWxlIgoJCQkJCgkJCQlhcGkgLT4gbWV0aG9kX3Byb2Nlc3MgInN0YXJ0cyIgIiIgIiNwcm9jZXNzIgoJCQkJCgkJCQllbmRfdXNlciAtPiBkYXNoX3VpICJ1c2VzIiAiIiAiI3VzZXIiCgkJCQkKCQkJCQoJCQkJZW5kX3VzZXIgLT4gcG9ydGFsICJ1c2VzIiAiIiAiI3VzZXIiCgkJCQlwb3J0YWwgLT4gZ2xvdyAibGF1bmNoZXMgVUkgZm9yIGV4aXN0aW5nIG9yIG5ldyBwcm9qZWN0IgoJCQkJZ2xvdyAtPiBwcm9kdWN0X2luc3RhbmNlX21hbmFnZXIgInJlcXVlc3RzIHN0YXJ0IGFuZCB0ZXJtaW5hdGlvbiBvZiBwcm9kdWN0IGluc3RhbmNlcyIgImdSUEMiICIjZ1JQQyIKCQkJCXByb2R1Y3RfaW5zdGFuY2VfbWFuYWdlciAtPiB0aGVpYSAibGF1bmNoZXMgVGhlaWEgdmlzdWFsaXphdGlvbiIgIiIgIiIKCQkJCWdsb3cgLT4gdGhlaWEgImxhdW5jaGVzIFRoZWlhIHZpc3VhbGl6YXRpb24iICIiICIiCgkJCQlwb3J0YWwgLT4gZ2xvdy5kYXNoX3VpICJsaW5rcyB0byIgIkphdmFTY3JpcHQgQ2xpY2sgSGFuZGxlciIgIiNsaW5rIgoJCQkJZ2xvdyAtPiBwcm9kdWN0ICJjYWxscyIgImdSUEMiICIjZ1JQQyIKCQkJCXByb2R1Y3Rfd3JpdGVzX3N0YXRlID0gcHJvZHVjdCAtPiBnbG93LnByb2plY3RzX2RpcmVjdG9yeSAicmVhZHMgJiB3cml0ZXMgcHJvZHVjdCBzdGF0ZSIgIiIgIiNmaWxlIgoJCQkJZW5kX3VzZXIgLT4gdGhlaWEgICJWaWV3cyBhbmQgaW50ZXJhY3RzIHdpdGggdGhlIDNEIG1vZGVsIgoJCQl9CgkJfQoJCQoJCWVuZF91c2VyX3dpbmRvd3NfcGNfID0gZGVwbG95bWVudEVudmlyb25tZW50ICJFbmQgVXNlciBXaW5kb3dzIERlc2t0b3AgUEMiIHsKCQkJZGVwbG95bWVudE5vZGUgIlB5dGhvbiBJbnRlcnByZXRlciIgInRoZSBweXRob24gaW50ZXJwcmV0ZXIgdGhhdCBydW5zIHRoZSBHTE9XIHNvbHV0aW9uIiAiUHl0aG9uIiAiIiAxIHsKCQkJCWRlcGxveW1lbnROb2RlICJPcmNoZXN0cmF0b3IiICJ0aGUgcHl0aG9uIG1vZHVsZSB0aGF0IHN0YXJ0cyBhbmQgc2h1dHNkb3duIHRoZSBHTE9XIHNvbHV0aW9uIiAiUHl0aG9uIiAiIiAxIHsKCQkJCQlhcGlfID0gY29udGFpbmVySW5zdGFuY2UgZ2xvdy5hcGkgIiIgewoJCQkJCX0KCQkJCQlkYXNoXyA9IGNvbnRhaW5lckluc3RhbmNlIGdsb3cuZGFzaCAiIiB7CgkJCQkJfQoJCQkJCW1ldGhvZF9wcm9jZXNzXyA9IGNvbnRhaW5lckluc3RhbmNlIGdsb3cubWV0aG9kX3Byb2Nlc3MgIiIgewoJCQkJCX0KCQkJCQlwb3J0YWxfc2VydmVyXyA9IGNvbnRhaW5lckluc3RhbmNlIHBvcnRhbC5wb3J0YWxfc2VydmVyICIiIHsKCQkJCQl9CgkJCQkJcHJvZHVjdF9pbnN0YW5jZV9tYW5hZ2VyXyA9IHNvZnR3YXJlU3lzdGVtSW5zdGFuY2UgcHJvZHVjdF9pbnN0YW5jZV9tYW5hZ2VyICIiIHsKCQkJCQl9CgkJCQl9CgkJCQlkZXBsb3ltZW50Tm9kZSAicHl3ZWJ2aWV3IiAiYSBweXRob24gYW5kIGJyb3dzZXIgYmFzZWQgZW5naW5lIGZvciByZW5kZXJpbmcgd2ViIFVJcyBhcyBkZXNrdG9wIGFwcGxpY2F0aW9uIHdpbmRvd3MiICJQeXRob24iICIiIDEgewoJCQkJCWRhc2hfdWlfID0gY29udGFpbmVySW5zdGFuY2UgZ2xvdy5kYXNoX3VpICIiIHsKCQkJCQl9CgkJCQkJcG9ydGFsX3VpXyA9IGNvbnRhaW5lckluc3RhbmNlIHBvcnRhbC51aSAiIiB7CgkJCQkJfQoJCQkJCXRoZWlhX3VpXyA9IGNvbnRhaW5lckluc3RhbmNlIHRoZWlhLnRoZWlhX2NsaWVudCAiIiAiIgoJCQkJfQoJCQl9CgkJCWRlcGxveW1lbnROb2RlICJGaWxlIFN5c3RlbSIgInRoZSBmaWxlIHN5c3RlbSBvZiBhIHNpbmdsZSBXaW5kb3dzIERlc2t0b3AgUEMiICJXaW5kb3dzIiAxIHsKCQkJCWRlcGxveW1lbnROb2RlICJVc2VyIERvY3VtZW50cyBEaXJlY3RvcnkiICJ0aGUgRG9jdW1lbnRzIGRpcmVjdG9yeSBvZiB0aGUgZW5kIHVzZXIiICJXaW5kb3dzIiAgMSB7CgkJCQkJcHJvamVjdHNfZGlyZWN0b3J5XyA9IGNvbnRhaW5lckluc3RhbmNlIGdsb3cucHJvamVjdHNfZGlyZWN0b3J5CgkJCQkJcHJvZHVjdF9pbnN0YW5jZV9maWxlX3NwYWNlXyA9IGNvbnRhaW5lckluc3RhbmNlIGdsb3cucHJvZHVjdF9pbnN0YW5jZV9maWxlX3NwYWNlCgkJCQl9CgkJCQlkZXBsb3ltZW50Tm9kZSAiQVBQREFUQSBEaXJlY3RvcnkiICJ0aGUgQVBQREFUQSBkaXJlY3Rvcnkgb2YgdGhlIGVuZCB1c2VyIiAiV2luZG93cyIgMSB7CgkJCQkJcHJvamVjdHNfZGF0YWJhc2VfID0gY29udGFpbmVySW5zdGFuY2UgZ2xvdy5wcm9qZWN0c19kYXRhYmFzZSAiCgkJCQl9CgkJCX0KCQl9CgkJCgl9CgkKCXZpZXdzIHsKCQl0aGVtZSBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vUlZSMDYvY29ybmlmZXItY29udHJpYi9tYWluL3RoZW1lcy9zZW1hbnRpYy90aGVtZS5qc29uCgkJdGhlbWUgaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1JWUjA2L2Nvcm5pZmVyLWNvbnRyaWIvbWFpbi90aGVtZXMvaGVyYWxkcnkvdGhlbWUuanNvbgoJCWJyYW5kaW5nIHsKCQkJbG9nbyBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vUlZSMDYvY29ybmlmZXItY29udHJpYi9tYWluL2Fzc2V0cy9hbnN5cy5wbmcKCQl9CgkJCgkJc3lzdGVtTGFuZHNjYXBlICJTeXN0ZW1MYW5kc2NhcGUiICJUaGVpYSBpbnRlZ3JhdGVkIGluIFNvbHV0aW9uIEFwcGxpY2F0aW9uIHVzaW5nIEdMT1ciIHsKCQkJaW5jbHVkZSAqCgkJCWF1dG9sYXlvdXQgbHIKCQl9CgkJCgkJc3lzdGVtQ29udGV4dCB0aGVpYSAiVGhlaWFTb2x1dGlvbkFwcGxpY2F0aW9uQ29udGV4dCIgIlRoZWlhIFNvbHV0aW9uIEFwcGxpY2F0aW9uIENvbnRleHQiIHsKCQkJaW5jbHVkZSAqCgkJCWluY2x1ZGUgZ2xvdwoJCQlpbmNsdWRlIHBvcnRhbAoJCQlhdXRvbGF5b3V0IGxyCgkJfQoJCQoJCWNvbnRhaW5lciB0aGVpYSAiVGhlaWFDb250YWluZXJzIiAiVGhlaWEgQ29udGFpbmVycyIgewoJCQlpbmNsdWRlIGVuZF91c2VyCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfY2xpZW50CgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfc2VydmVyCgkJfQoJCQoJCXN5c3RlbUNvbnRleHQgdGhlaWEgIlRoZWlhU3lzdGVtQ29udGV4dCIgIlRoZWlhIENvbnRleHQiIHsKCQkJaW5jbHVkZSAqCgkJCWF1dG9sYXlvdXQgbHIKCQl9CgkJCgkJCgkJY29tcG9uZW50IHRoZWlhLnRoZWlhX3NlcnZlciAiVGhlaWFTZXJ2ZXJDb21wb25lbnRzIiAiVGhlaWEgU2VydmVyIENvbXBvbmVudHMiIHsKCQkJaW5jbHVkZSAqCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfc2VydmVyLnRoZWlhX2h0dHBfYXBpCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfc2VydmVyLnRoZWlhX3NlcnZlcl9jb21wb25lbnQKCQkJaW5jbHVkZSB0aGVpYS50aGVpYV9zZXJ2ZXIudHJhbWVfc2VydmVyCgkJCWF1dG9sYXlvdXQgbHIKCQl9CgkJCgkJY29tcG9uZW50IHRoZWlhLnRoZWlhX2Rhc2ggIlRoZWlhRGFzaENvbXBvbmVudHMiICJUaGVpYSBEYXNoIFVJIENvbXBvbmVudHMiIHsKCQkJaW5jbHVkZSAqCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfZGFzaC50aGVpYV9kYXNoX3VpCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfZGFzaC50aGVpYV9kYXNoX2FwaQoJCQlpbmNsdWRlIHRoZWlhLnRoZWlhX2Rhc2gudGhlaWFfY2xpZW50X2FwaQoJCQlpbmNsdWRlIHRoZWlhLnRoZWlhX2Rhc2gudGhlaWFfanNfbGlicmFyeQoJCQlhdXRvbGF5b3V0IGxyCgkJfQoJCQoJCWNvbXBvbmVudCB0aGVpYS50aGVpYV9jbGllbnQgIlRoZWlhQ2xpZW50Q29tcG9uZW50cyIgIlRoZWlhIENsaWVudCBDb21wb25lbnQiIHsKCQkJaW5jbHVkZSAqCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfY2xpZW50LnRoZWlhX3VpX2VsZW1lbnRzCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfY2xpZW50LnRoZWlhX3NjZW5lX2NvbXBvbmVudAoJCQlpbmNsdWRlIHRoZWlhLnRoZWlhX2NsaWVudC50aGVpYV90cmFtZV9mdW5jdGlvbmFsaXR5X2FwaQoJCQlpbmNsdWRlIHRoZWlhLnRyYW1lX3Z0a19sb2NhbF9jb250YWluZXIudHJhbWVfd2FzbV9oYW5kbGVyCgkJCWluY2x1ZGUgdGhlaWEudHJhbWVfdnRrX2xvY2FsX2NvbnRhaW5lci50cmFtZV93c2xpbmtfY29ubmVjdGlvbgoJCQlhdXRvbGF5b3V0IGxyCgkJfQoJCQoJCWNvbXBvbmVudCB0aGVpYS50aGVpYV9hcHAgIlRoZWlhQXBwQ29tcG9uZW50cyIgIlRoZWlhIEFwcGxpY2F0aW9uIENvbXBvbmVudHMiIHsKCQkJaW5jbHVkZSAqCgkJCWluY2x1ZGUgdGhlaWEudGhlaWFfYXBwLnRoZWlhX2FwaQoJCQlpbmNsdWRlIHRoZWlhLnRoZWlhX2FwcC50cmFtZV9hcHBsaWNhdGlvbgoJCQlpbmNsdWRlIHRoZWlhLnRoZWlhX2FwcC52dGtfcGlwZWxpbmUKCQkJaW5jbHVkZSB0aGVpYS50aGVpYV9hcHAuc2NlbmVfZ3JhcGgKCQkJaW5jbHVkZSB0aGVpYS50aGVpYV9hcHAudGhlaWFfbG9nbW9uaXRvcgoJCQlhdXRvbGF5b3V0IGxyCgkJfQoJCQoJCWRlcGxveW1lbnQgKiBlbmRfdXNlcl93aW5kb3dzX3BjXyAiRW5kVXNlckRlcGxveW1lbnQiICJFbmQgVXNlciBEZXBsb3ltZW50IiB7CgkJCWluY2x1ZGUgKgoJCQlhdXRvbGF5b3V0IGxyCgkJfQoJfQo=" + }, + "configuration" : { }, + "model" : { + "people" : [ { + "id" : "1", + "tags" : "Element,Person", + "properties" : { + "structurizr.dsl.identifier" : "end_user" + }, + "name" : "End User", + "description" : "A person who is using a Solution", + "relationships" : [ { + "id" : "42", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "78cf0108-dd79-464a-a883-2b8f31842cc4" + }, + "sourceId" : "1", + "destinationId" : "4", + "description" : "Triggers 3D model view updates" + }, { + "id" : "102", + "tags" : "Relationship,#user", + "properties" : { + "structurizr.dsl.identifier" : "f27f4964-64a6-45a0-bb1a-3e8b150985f6" + }, + "sourceId" : "1", + "destinationId" : "54", + "description" : "uses" + }, { + "id" : "103", + "tags" : "Relationship,#user", + "properties" : { + "structurizr.dsl.identifier" : "831ca93a-5340-4c86-bf75-ebc1ddf28603" + }, + "sourceId" : "1", + "destinationId" : "48", + "description" : "uses" + }, { + "id" : "43", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "04182223-e5a1-4a03-b353-ab4b7a39838f" + }, + "sourceId" : "1", + "destinationId" : "5", + "description" : "Triggers 3D model view updates" + }, { + "id" : "111", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "154d7a26-d58c-490c-a187-a79d71d0d205" + }, + "sourceId" : "1", + "destinationId" : "2", + "description" : "Views and interacts with the 3D model" + } ], + "location" : "Unspecified" + } ], + "softwareSystems" : [ { + "id" : "2", + "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "visor" + }, + "name" : "Visor", + "description" : "3D Viewer for Solutions Applications", + "group" : "Ansys Corporate Client", + "location" : "Unspecified", + "containers" : [ { + "id" : "8", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_client" + }, + "name" : "VISOR JS client", + "description" : "VISOR client implementing the viewer functionality on the frontend using Trame VTK.WASM module library.", + "relationships" : [ { + "id" : "44", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "6d8d1933-ef63-48c3-a320-06721693e22f" + }, + "sourceId" : "8", + "destinationId" : "1", + "description" : "Visualization of 3D model data" + }, { + "id" : "46", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "585379d4-0bad-4167-b5a6-6e7bf8d80b46" + }, + "sourceId" : "8", + "destinationId" : "15", + "description" : "Requests model data" + }, { + "id" : "45", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "9a712a84-c38d-452d-9e84-13c5a1dac9d4" + }, + "sourceId" : "8", + "destinationId" : "1", + "description" : "Updates 3D model view" + } ], + "group" : "VISOR Client", + "technology" : "Typescript,JavaScript,React,MJS,Trame,WASM,VTK.WASM", + "components" : [ { + "id" : "10", + "tags" : "Element,Component,#React,#Typescript", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_client.visor_ui_elements" + }, + "name" : "VISOR UI Elements", + "description" : "VISOR UI elements for the VISOR viewer", + "technology" : "React,Typescript", + "documentation" : { } + }, { + "id" : "9", + "tags" : "Element,Component,#Typescript,#React,#VTK", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_client.visor_scene_component" + }, + "name" : "VISOR Viewer Scene Graph component", + "description" : "VISOR 3D Viewer scene graph component for visualization of the model topology", + "technology" : "VTK, Typescript, React", + "documentation" : { } + }, { + "id" : "11", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_client.visor_trame_functionality_api" + }, + "name" : "VISOR Trame application sync and state manager", + "description" : "VISOR API for interfacing with the VISOR defined Trame application on the server", + "relationships" : [ { + "id" : "38", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "c46e942a-d12f-420e-aec0-48b6d7b70e96" + }, + "sourceId" : "11", + "destinationId" : "16", + "description" : "Trigger VTK updates" + } ], + "technology" : "TypeScript,Trame,React", + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "19", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_app" + }, + "name" : "VISOR Application Component", + "description" : "VISOR Application controlling the choice of rendering engine, a VISOR Server instance and providing the Python API to the application", + "relationships" : [ { + "id" : "26", + "tags" : "Relationship,#openapi", + "properties" : { + "structurizr.dsl.identifier" : "840e7b6f-651b-4c60-9253-c81dac235ba3" + }, + "sourceId" : "19", + "destinationId" : "17", + "description" : "Provides an HTTP API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection." + } ], + "technology" : "Python", + "components" : [ { + "id" : "21", + "tags" : "Element,Component,tags", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_app.trame_application" + }, + "name" : "Trame application", + "description" : "Trame application based on Trame vtk_local application utilizing VTK.WASM and a VTK Object Manager for client-server synchronization", + "relationships" : [ { + "id" : "28", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "97527f45-f2f8-4141-94ca-3c20a62885f1" + }, + "sourceId" : "21", + "destinationId" : "22", + "description" : "Sets up the VTK pipeline for the server side and synchronizes with the client side" + }, { + "id" : "29", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "ce876782-2b7a-4b55-b968-77bf9143732c" + }, + "sourceId" : "21", + "destinationId" : "23", + "description" : "Sets up the scene graph for the server side" + } ], + "technology" : "technology", + "documentation" : { } + }, { + "id" : "22", + "tags" : "Element,Component,#VTK", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_app.vtk_pipeline" + }, + "name" : "VTK Pipeline and shared objects with the client side", + "description" : "VTK pipeline setup for visualization on the server side which is synchronized with the VTK rendering on the client side", + "technology" : "VTK", + "documentation" : { } + }, { + "id" : "20", + "tags" : "Element,Component,#openapi,#fastapi", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_app.visor_api" + }, + "name" : "VISOR API", + "description" : "VISOR API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection.", + "relationships" : [ { + "id" : "31", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "d5f3a346-dc04-4d4a-8b7e-563ed5683f0e" + }, + "sourceId" : "20", + "destinationId" : "21", + "description" : "Manages the Trame application client and server side, along with the VTK pipeline, scene management and input management." + } ], + "technology" : "OpenAPI, FastAPI", + "documentation" : { } + }, { + "id" : "24", + "tags" : "Element,Component,#Python,#OpenTelemetry", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_app.visor_logmonitor" + }, + "name" : "VISOR Logger and Monitor of the application, servers and services", + "description" : "VISOR logger and monitor is the part of the VISOR application which implements the OpenTelemetry standards for VISOR", + "relationships" : [ { + "id" : "30", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "0ddf1ba3-56ae-4bd0-9546-52c7bbf73eef" + }, + "sourceId" : "24", + "destinationId" : "21", + "description" : "Monitors the application and server" + } ], + "technology" : "Python,OpenTelemetry", + "documentation" : { } + }, { + "id" : "23", + "tags" : "Element,Component,#Python,#VTK", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_app.scene_graph" + }, + "name" : "Scene graph", + "description" : "Scene graph for supporting visualization of object hierarchies and scene attributes between the client and the server side", + "relationships" : [ { + "id" : "32", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "7936a240-6d66-4c6c-ad7a-b47def82cd2f" + }, + "sourceId" : "23", + "destinationId" : "9", + "description" : "Updates view and sends events to UI elements" + } ], + "technology" : "Python, VTK", + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "15", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_server" + }, + "name" : "VISOR server", + "description" : "VISOR server supporting 3D rendering of models from Ansys flagship products", + "relationships" : [ { + "id" : "89", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "f9285d4b-b30f-48bd-badd-0af458310da6" + }, + "sourceId" : "15", + "destinationId" : "76", + "description" : "reads model data" + }, { + "id" : "88", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "224a74b6-a463-4511-9f48-5edce6982c46" + }, + "sourceId" : "15", + "destinationId" : "75", + "description" : "reads model data" + }, { + "id" : "47", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "2846a074-dde3-47ba-b753-9e61d3672b8f" + }, + "sourceId" : "15", + "destinationId" : "8", + "description" : "Sends model data" + } ], + "components" : [ { + "id" : "18", + "tags" : "Element,Component,#Python,#WebSocket,#wslink", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_server.visor_server_component" + }, + "name" : "VISOR Server Component", + "description" : "VISOR server component creating a Trame server for a single session for this VISOR application", + "relationships" : [ { + "id" : "36", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "0bf8553e-9365-4e50-afce-0b6fc91121a7" + }, + "sourceId" : "18", + "destinationId" : "16", + "description" : "Lifecycle management of the Trame server" + } ], + "technology" : "Python, WebSocket, wslink", + "documentation" : { } + }, { + "id" : "16", + "tags" : "Element,Component,#Python,#WebSocket,#wslink", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_server.trame_server" + }, + "name" : "Trame Server", + "description" : "Trame server component supporting single session using a web socket connection", + "relationships" : [ { + "id" : "37", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "9309941b-2b11-40b2-a3f6-81b3c12493ba" + }, + "sourceId" : "16", + "destinationId" : "14", + "description" : "Sends scene updates" + } ], + "technology" : "Python, WebSocket, wslink", + "documentation" : { } + }, { + "id" : "17", + "tags" : "Element,Component,#openapi,#fastapi", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_server.visor_http_api" + }, + "name" : "VISOR Server Orchestration HTTP API", + "description" : "Provides an HTTP API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection.", + "relationships" : [ { + "id" : "25", + "tags" : "Relationship,#openapi", + "properties" : { + "structurizr.dsl.identifier" : "18002b43-c7cc-44d6-80c7-5e3a8dee3ef8" + }, + "sourceId" : "17", + "destinationId" : "20", + "description" : "Provides an HTTP API for controlling the lifecycle of the starting, stopping or updating the Trame server and the websocket connection." + }, { + "id" : "41", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "fea92b37-afdb-4a21-b1fa-7b45fc0d8d2a" + }, + "sourceId" : "17", + "destinationId" : "18", + "description" : "Controls server start, stop and state updates as well as monitoring tasks." + } ], + "technology" : "OpenAPI,FastAPI", + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "3", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_dash" + }, + "name" : "VISOR 3D Viewer Dash UI component", + "description" : "VISOR 3D Viewer Dash wrapper with Python bindings for the VISOR web client library and client api", + "technology" : "Dash, Python, Typescript, React, VTK WASM JS viewer library", + "components" : [ { + "id" : "6", + "tags" : "Element,Component,#React,#TypeScript", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_dash.visor_client_api" + }, + "name" : "VISOR client UI API", + "description" : "Implements an API which interfaces and implements actions on the web UI, trame vtk module library and/or the scene component.", + "relationships" : [ { + "id" : "33", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "29c996be-022d-4555-b493-cdf37c3ffbe3" + }, + "sourceId" : "6", + "destinationId" : "11", + "description" : "Triggers functionality from the Dash client to the VISOR client library which is either a web UI functionality, or a Trame VTK.WASM functionality synchronized with the server and/or functionality on the scene component" + } ], + "technology" : "React,TypeScript", + "documentation" : { } + }, { + "id" : "7", + "tags" : "Element,Component,#JavaScript,#TypeScript,#React", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_dash.visor_js_library" + }, + "name" : "VISOR JS library", + "description" : "VISOR JS library implementing the visor client", + "technology" : "JavaScript,TypeScript,React", + "documentation" : { } + }, { + "id" : "4", + "tags" : "Element,Component,#React,#Typescript,#JS", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_dash.visor_dash_ui" + }, + "name" : "VISOR Viewer web client library", + "description" : "React TypeScript library of the VISOR client UI", + "relationships" : [ { + "id" : "27", + "tags" : "Relationship,#React,#Typescript,#JavaScript", + "properties" : { + "structurizr.dsl.identifier" : "d4e3f0d6-f53f-4e68-8153-604e5281edb1" + }, + "sourceId" : "4", + "destinationId" : "7", + "description" : "Triggers functionality from the Dash client to the VISOR client" + } ], + "technology" : "React,Typescript,Javascript", + "documentation" : { } + }, { + "id" : "5", + "tags" : "Element,Component,#Dash,#JS,#Typescript,#React", + "properties" : { + "structurizr.dsl.identifier" : "visor.visor_dash.visor_dash_api" + }, + "name" : "VISOR Dash UI Component API", + "description" : "Provides an API through the React interface available through the Dash component in order to allow for client side callbacks triggering specific functionality from the Dash application", + "relationships" : [ { + "id" : "35", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "4205c7a5-e060-4613-a92b-0470dea3314f" + }, + "sourceId" : "5", + "destinationId" : "11", + "description" : "Triggers functionality from the Dash client to the VISOR client library utilizing Trame VTK.WASM functionality on the client or server side." + }, { + "id" : "96", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "ed78ec43-26b9-4d20-b65d-143bc070e5e5" + }, + "sourceId" : "5", + "destinationId" : "54", + "description" : "Sends data to the Dash UI and status responses based on user interaction" + }, { + "id" : "34", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "4614eed6-3107-4650-b9e1-000e9b4ac927" + }, + "sourceId" : "5", + "destinationId" : "9", + "description" : "Triggers visualization updates" + } ], + "technology" : "Dash,React,TypeScript", + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "12", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "visor.trame_vtk_local_container" + }, + "name" : "Trame VTK.WASM library", + "group" : "VISOR Client", + "components" : [ { + "id" : "14", + "tags" : "Element,Component,#VTK,#WASM,#JS,#MJS", + "properties" : { + "structurizr.dsl.identifier" : "visor.trame_vtk_local_container.trame_wasm_handler" + }, + "name" : "Trame Object Manager", + "description" : "VTK Object manager for serializaton/deserialization of VTK C++ classes for VTK pipeline objects shared between the client and the Trame server", + "relationships" : [ { + "id" : "39", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "31f1f834-3494-4414-81c1-041b58015154" + }, + "sourceId" : "14", + "destinationId" : "16", + "description" : "Triggers VTK updates" + } ], + "technology" : "VTK.WASM,JS,MJS", + "documentation" : { } + }, { + "id" : "13", + "tags" : "Element,Component,#Python,#WebSocket,#wslink", + "properties" : { + "structurizr.dsl.identifier" : "visor.trame_vtk_local_container.trame_wslink_connection" + }, + "name" : "Trame WSLINK connection and WASM loader", + "description" : "Trame WSLINK connection to the server and WASM loader", + "relationships" : [ { + "id" : "40", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "c1819b9b-ce45-4228-ad1d-5608dc9537cb" + }, + "sourceId" : "13", + "destinationId" : "16", + "description" : "Connects to running wslink session to setup a websocket connection." + } ], + "technology" : "Python, WebSocket, wslink", + "documentation" : { } + } ], + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "51", + "tags" : "Element,Software System", + "url" : "https://tfs.ansys.com:8443/tfs/ANSYS_Development/Extensibility/_git/Root?path=%2Fansys%2Finstancemanagement%2Flight", + "properties" : { + "structurizr.dsl.identifier" : "product_instance_manager" + }, + "name" : "Product Instance Manager", + "description" : "enables the startup and termination of Ansys Flagship Products or other stateful processes", + "relationships" : [ { + "id" : "86", + "tags" : "Relationship,#process", + "properties" : { + "structurizr.dsl.identifier" : "a757b535-32fe-45c9-9fc9-27bff39c5453" + }, + "sourceId" : "51", + "destinationId" : "52", + "description" : "starts & kills" + }, { + "id" : "106", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "b60c9503-2048-4d84-9a4d-a3c1dd269ef3" + }, + "sourceId" : "51", + "destinationId" : "2", + "description" : "launches VISOR visualization" + }, { + "id" : "87", + "tags" : "Relationship,#process", + "properties" : { + "structurizr.dsl.identifier" : "a8ddf3b6-6347-412f-be88-561ce6bb0238" + }, + "sourceId" : "51", + "destinationId" : "15", + "description" : "Starts, stops viewer" + } ], + "group" : "Ansys Corporate Client", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "48", + "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "portal" + }, + "name" : "SAF Portal", + "description" : "enables the user to create new project or select existing project then launch solution UI for project. Does not have responsibility for implementation of any aspect of the solution business logic or the services consumed by the solution.", + "relationships" : [ { + "id" : "108", + "tags" : "Relationship,#link", + "properties" : { + "structurizr.dsl.identifier" : "4a2daf08-d0a0-4289-ae64-13bbe1abaa7e" + }, + "sourceId" : "48", + "destinationId" : "54", + "description" : "links to", + "technology" : "JavaScript Click Handler" + }, { + "id" : "97", + "tags" : "Relationship,#REST", + "properties" : { + "structurizr.dsl.identifier" : "a7835452-f305-4688-a194-98f921228e66" + }, + "sourceId" : "48", + "destinationId" : "56", + "description" : "Call", + "technology" : "REST" + }, { + "id" : "104", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "4cdd2e14-ad9a-4cc6-a255-cfc5815b2c7e" + }, + "sourceId" : "48", + "destinationId" : "53", + "description" : "launches UI for existing or new project" + } ], + "group" : "Ansys Corporate Client", + "location" : "Unspecified", + "containers" : [ { + "id" : "49", + "tags" : "Element,Container,#fastapi", + "properties" : { + "structurizr.dsl.identifier" : "portal.portal_server" + }, + "name" : "Portal Server", + "description" : "implements a REST API that is consumed by the Portal UI. The portal server consumes a small subset of the API provided by the GLOW API Server", + "technology" : "FastAPI", + "documentation" : { } + }, { + "id" : "50", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "portal.ui" + }, + "name" : "Portal User Interface", + "description" : "provides a view of the projects in the projects directory. enables the user to create or select a project then launch solution UI for the project", + "technology" : "React", + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "52", + "tags" : "Element,Software System,#external", + "properties" : { + "structurizr.dsl.identifier" : "product" + }, + "name" : "Ansys Flagship Product", + "description" : "A stateful process that is required to implement a GLOW transaction method (typically an Ansys Flagship product which contains a simulation solver designed to be a desktop application)", + "relationships" : [ { + "id" : "91", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "48f55a90-865f-4819-a29f-ca7a9bfd504c" + }, + "sourceId" : "52", + "destinationId" : "76", + "description" : "writes 3D model" + }, { + "id" : "110", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "product_writes_state" + }, + "sourceId" : "52", + "destinationId" : "55", + "description" : "reads & writes product state" + }, { + "id" : "90", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "34c98483-7617-4a8d-887d-cbd72935586c" + }, + "sourceId" : "52", + "destinationId" : "75", + "description" : "writes 3D model" + } ], + "group" : "Ansys Corporate Client", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "53", + "tags" : "Element,Software System", + "url" : "https://github.com/ansys-internal/glow-engine", + "properties" : { + "structurizr.dsl.identifier" : "glow" + }, + "name" : "Guided Low Code Workflow (GLOW)", + "description" : "framework for vertical applications orientated towards a guided workflow user experience", + "relationships" : [ { + "id" : "107", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "ac953e71-5a6b-45f4-aae8-06d1cb44dd61" + }, + "sourceId" : "53", + "destinationId" : "2", + "description" : "launches VISOR visualization" + }, { + "id" : "105", + "tags" : "Relationship,#gRPC", + "properties" : { + "structurizr.dsl.identifier" : "9cd50811-7c3b-48eb-ab8d-46a0ea995ec1" + }, + "sourceId" : "53", + "destinationId" : "51", + "description" : "requests start and termination of product instances", + "technology" : "gRPC" + }, { + "id" : "109", + "tags" : "Relationship,#gRPC", + "properties" : { + "structurizr.dsl.identifier" : "c02cb3bf-1712-41f1-a5f0-21404938a79a" + }, + "sourceId" : "53", + "destinationId" : "52", + "description" : "calls", + "technology" : "gRPC" + } ], + "group" : "Ansys Corporate Client", + "location" : "Unspecified", + "containers" : [ { + "id" : "76", + "tags" : "Element,Container,#file", + "properties" : { + "structurizr.dsl.identifier" : "glow.product_instance_file_space" + }, + "name" : "Product Instance file space", + "description" : "the OS directory associated with a product instance", + "technology" : "file system directory", + "documentation" : { } + }, { + "id" : "56", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "glow.api" + }, + "name" : "API Server", + "description" : "Provides a REST API specific to a given solution, which is consumed by the solution UI server.", + "relationships" : [ { + "id" : "77", + "tags" : "Relationship,#process", + "properties" : { + "structurizr.dsl.identifier" : "72363f91-5f97-4c68-991d-d0605af7af89" + }, + "sourceId" : "56", + "destinationId" : "69", + "description" : "starts & stops" + }, { + "id" : "101", + "tags" : "Relationship,#process", + "properties" : { + "structurizr.dsl.identifier" : "3b5e8a04-0b53-4d0c-aa07-4216046a778c" + }, + "sourceId" : "56", + "destinationId" : "69", + "description" : "starts" + }, { + "id" : "84", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "35737807-caa5-4b47-8c33-261f88ddf6ba" + }, + "sourceId" : "56", + "destinationId" : "57", + "description" : "gets, modifies & creates records in" + }, { + "id" : "83", + "tags" : "Relationship,#gRPC", + "properties" : { + "structurizr.dsl.identifier" : "1c69aaee-673a-472e-8936-572ace4d58ce" + }, + "sourceId" : "56", + "destinationId" : "51", + "description" : "requests product termination (on shutdown)", + "technology" : "gRPC" + }, { + "id" : "85", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "df363bcc-842a-4f98-b61b-bae7a0c5ce99" + }, + "sourceId" : "56", + "destinationId" : "55", + "description" : "reads and writes project files" + } ], + "group" : "API", + "documentation" : { } + }, { + "id" : "75", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "glow.method_file_space" + }, + "name" : "Method file space", + "description" : "the temporary directory used by a method execution process that exists just for the duration of the process.", + "technology" : "file sdystem directory", + "documentation" : { } + }, { + "id" : "55", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "glow.projects_directory" + }, + "name" : "Projects Directory", + "description" : "the file system directory containing project files", + "group" : "API", + "documentation" : { } + }, { + "id" : "57", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "glow.projects_database" + }, + "name" : "Projects Database", + "description" : "stores instances of the solution schema", + "group" : "API", + "documentation" : { } + }, { + "id" : "69", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "glow.method_process" + }, + "name" : "Method Execution Process", + "description" : "An OS process that implements a single call to a transaction method", + "relationships" : [ { + "id" : "98", + "tags" : "Relationship,#REST", + "properties" : { + "structurizr.dsl.identifier" : "165fcd02-6e54-4bd6-8873-9d9147492c60" + }, + "sourceId" : "69", + "destinationId" : "56", + "description" : "uploads and downloads fields", + "technology" : "REST" + }, { + "id" : "100", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "36cf8549-7e3c-40ea-bcfb-9be5f28e396a" + }, + "sourceId" : "69", + "destinationId" : "76", + "description" : "creates and deletes" + }, { + "id" : "82", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "f3cd4743-f9b7-4ec5-9cf6-d8a594c72a9b" + }, + "sourceId" : "69", + "destinationId" : "76", + "description" : "creates & deletes" + }, { + "id" : "99", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "1ae75136-a60d-4823-994f-ae08b47fe9e9" + }, + "sourceId" : "69", + "destinationId" : "75", + "description" : "creates and deletes" + }, { + "id" : "79", + "tags" : "Relationship,#gRPC", + "properties" : { + "structurizr.dsl.identifier" : "c2c025d7-617a-474c-b355-d56198a3e83e" + }, + "sourceId" : "69", + "destinationId" : "51", + "description" : "queries product connection, requests product start & termination", + "technology" : "gRPC" + }, { + "id" : "78", + "tags" : "Relationship,#gRPC", + "properties" : { + "structurizr.dsl.identifier" : "719626ea-3c34-4acb-a549-0eaac66bac96" + }, + "sourceId" : "69", + "destinationId" : "52", + "description" : "executes method code", + "technology" : "gRPC" + }, { + "id" : "80", + "tags" : "Relationship,#REST", + "properties" : { + "structurizr.dsl.identifier" : "42151dd2-0ad2-42f9-9ca0-3a5cccd0eabb" + }, + "sourceId" : "69", + "destinationId" : "56", + "description" : "calls", + "technology" : "REST" + }, { + "id" : "81", + "tags" : "Relationship,#file", + "properties" : { + "structurizr.dsl.identifier" : "dbd0b58d-7179-497e-9e5f-93c9137842b8" + }, + "sourceId" : "69", + "destinationId" : "75", + "description" : "creates & deletes" + } ], + "technology" : "Python", + "components" : [ { + "id" : "71", + "tags" : "Element,Component", + "url" : "https://github.com/ansys-internal/glow-engine/tree/main/src/ansys/saf/glow/solution", + "properties" : { + "structurizr.dsl.identifier" : "glow.method_process.solution_definition_api" + }, + "name" : "Solution definition API", + "description" : "a python package that contains the set of python types required to define a GLOW solution", + "technology" : "Python", + "documentation" : { } + }, { + "id" : "70", + "tags" : "Element,Component", + "url" : "https://github.com/ansys-internal/glow-engine/blob/main/src/ansys/saf/glow/_executor/method_runner.py#L41", + "properties" : { + "structurizr.dsl.identifier" : "glow.method_process.method_runner" + }, + "name" : "Method Runner", + "description" : "implements a single call to a transaction method", + "relationships" : [ { + "id" : "73", + "tags" : "Relationship,#function", + "properties" : { + "structurizr.dsl.identifier" : "01b38730-e7e3-4fa6-8405-2f2dd954a5fe" + }, + "sourceId" : "70", + "destinationId" : "72", + "description" : "executes method" + } ], + "technology" : "Python", + "documentation" : { } + }, { + "id" : "72", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "glow.method_process.solution" + }, + "name" : "Solution definition", + "description" : "the definition of a solution's schema and business logic", + "relationships" : [ { + "id" : "74", + "tags" : "Relationship,#import", + "properties" : { + "structurizr.dsl.identifier" : "70dc40d0-4943-4f4e-a334-235450d99be4" + }, + "sourceId" : "72", + "destinationId" : "71", + "description" : "obtains base types for solution definition" + } ], + "technology" : "Python", + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "54", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "glow.dash_ui" + }, + "name" : "Solution Dash UI", + "description" : "A browser based client for the Dash server implemented in React Javascript that renders the UI defined by the Dash server", + "relationships" : [ { + "id" : "92", + "tags" : "Relationship,#REST", + "properties" : { + "structurizr.dsl.identifier" : "23fc0608-050d-4efd-ba56-6542c577b4a1" + }, + "sourceId" : "54", + "destinationId" : "58", + "description" : "obtains code and state; signals user interface events", + "technology" : "REST" + }, { + "id" : "94", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "e4eee6d2-4afc-4d6c-803a-28ddf3e59460" + }, + "sourceId" : "54", + "destinationId" : "4", + "description" : "Signals user interface events" + }, { + "id" : "95", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "b80fb8d4-95f7-42e4-bade-878fb1e3a1e5" + }, + "sourceId" : "54", + "destinationId" : "5", + "description" : "Triggers visual events and requests data from the VISOR 3D viewer" + } ], + "technology" : "React", + "documentation" : { } + }, { + "id" : "58", + "tags" : "Element,Container,#Flask", + "properties" : { + "structurizr.dsl.identifier" : "glow.dash" + }, + "name" : "Dash Server", + "description" : "a Flask server that services a React browser based UI defined using the Dash UI definition API", + "relationships" : [ { + "id" : "93", + "tags" : "Relationship,#REST", + "properties" : { + "structurizr.dsl.identifier" : "f9ca44bb-b9e9-4cde-a3c0-425d6dd49f98" + }, + "sourceId" : "58", + "destinationId" : "56", + "description" : "Call", + "technology" : "REST" + } ], + "technology" : "Flask", + "components" : [ { + "id" : "63", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "glow.dash.solution" + }, + "name" : "Solution definition", + "description" : "the definition of a solution's schema and business logic", + "relationships" : [ { + "id" : "67", + "tags" : "Relationship,#import", + "properties" : { + "structurizr.dsl.identifier" : "17e717b6-cacc-4635-b689-342fb10a2083" + }, + "sourceId" : "63", + "destinationId" : "62", + "description" : "obtains base types for solution definition" + } ], + "technology" : "Python", + "documentation" : { } + }, { + "id" : "60", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "glow.dash.solution_ui" + }, + "name" : "Solution UI", + "description" : "a python package which defines how the solution is rendered via the Dash UI definition API", + "relationships" : [ { + "id" : "64", + "tags" : "Relationship,#import", + "properties" : { + "structurizr.dsl.identifier" : "fc36a023-4430-4c65-9d63-d57f05e4116e" + }, + "sourceId" : "60", + "destinationId" : "59", + "description" : "invoke rendering providing UI structure and callbacks" + }, { + "id" : "65", + "tags" : "Relationship,#function", + "properties" : { + "structurizr.dsl.identifier" : "f81ba5e7-6280-4564-a60e-2eea048a3f8d" + }, + "sourceId" : "60", + "destinationId" : "61", + "description" : "gets and sets data; and invokes methods via proxy objects" + } ], + "technology" : "Python", + "documentation" : { } + }, { + "id" : "59", + "tags" : "Element,Component", + "url" : "https://dash.plotly.com/", + "properties" : { + "structurizr.dsl.identifier" : "glow.dash.dash_flask_server" + }, + "name" : "Dash Flask Server", + "description" : "a Flask server that services a React browser based UI defined using the Dash UI definition API", + "technology" : "Flask", + "documentation" : { } + }, { + "id" : "61", + "tags" : "Element,Component", + "url" : "https://github.com/ansys-internal/glow-engine/tree/main/src/ansys/saf/glow/client", + "properties" : { + "structurizr.dsl.identifier" : "glow.dash.client_api" + }, + "name" : "Client API", + "description" : "a python package that provides a pythonic interface to a GLOW API server via REST", + "relationships" : [ { + "id" : "68", + "tags" : "Relationship,#REST", + "properties" : { + "structurizr.dsl.identifier" : "10d9c09e-664f-4480-b81d-faf1967981af" + }, + "sourceId" : "61", + "destinationId" : "56", + "description" : "calls", + "technology" : "REST" + }, { + "id" : "66", + "tags" : "Relationship,#import", + "properties" : { + "structurizr.dsl.identifier" : "892a02f8-903f-4fbe-a414-9de1ecd72d90" + }, + "sourceId" : "61", + "destinationId" : "63", + "description" : "obtains schema and method set" + } ], + "technology" : "Python", + "documentation" : { } + }, { + "id" : "62", + "tags" : "Element,Component", + "url" : "https://github.com/ansys-internal/glow-engine/tree/main/src/ansys/saf/glow/solution", + "properties" : { + "structurizr.dsl.identifier" : "glow.dash.solution_definition_api" + }, + "name" : "GLOW Solution definition API", + "description" : "a python package that contains the set of python types required to define a GLOW solution", + "technology" : "Python", + "documentation" : { } + } ], + "documentation" : { } + } ], + "documentation" : { } + } ], + "deploymentNodes" : [ { + "id" : "131", + "tags" : "Element,Deployment Node,1", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.766479b0-ca64-4b58-bf66-013777ad47c2" + }, + "name" : "File System", + "description" : "the file system of a single Windows Desktop PC", + "environment" : "End User Windows Desktop PC", + "technology" : "Windows", + "instances" : "1", + "children" : [ { + "id" : "138", + "tags" : "Element,Deployment Node,1", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.766479b0-ca64-4b58-bf66-013777ad47c2.696158b9-9b7a-4053-9c95-645a8ed9fe7e" + }, + "name" : "APPDATA Directory", + "description" : "the APPDATA directory of the end user", + "environment" : "End User Windows Desktop PC", + "technology" : "Windows", + "instances" : "1", + "containerInstances" : [ { + "id" : "139", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.766479b0-ca64-4b58-bf66-013777ad47c2.696158b9-9b7a-4053-9c95-645a8ed9fe7e.projects_database_" + }, + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "57" + } ] + }, { + "id" : "132", + "tags" : "Element,Deployment Node,1", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.766479b0-ca64-4b58-bf66-013777ad47c2.7da33f8f-9925-4746-937b-32937a315ae8" + }, + "name" : "User Documents Directory", + "description" : "the Documents directory of the end user", + "environment" : "End User Windows Desktop PC", + "technology" : "Windows", + "instances" : "1", + "containerInstances" : [ { + "id" : "135", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.766479b0-ca64-4b58-bf66-013777ad47c2.7da33f8f-9925-4746-937b-32937a315ae8.product_instance_file_space_" + }, + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "76" + }, { + "id" : "133", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.766479b0-ca64-4b58-bf66-013777ad47c2.7da33f8f-9925-4746-937b-32937a315ae8.projects_directory_" + }, + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "55" + } ] + } ] + }, { + "id" : "112", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2" + }, + "name" : "Python Interpreter", + "description" : "the python interpreter that runs the GLOW solution", + "environment" : "End User Windows Desktop PC", + "technology" : "Python", + "instances" : "1", + "children" : [ { + "id" : "126", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.9a758bd9-e31b-45b4-94d3-cdbfaa9be8d6" + }, + "name" : "pywebview", + "description" : "a python and browser based engine for rendering web UIs as desktop application windows", + "environment" : "End User Windows Desktop PC", + "technology" : "Python", + "instances" : "1", + "containerInstances" : [ { + "id" : "127", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.9a758bd9-e31b-45b4-94d3-cdbfaa9be8d6.dash_ui_" + }, + "relationships" : [ { + "id" : "128", + "sourceId" : "127", + "destinationId" : "115", + "description" : "obtains code and state; signals user interface events", + "technology" : "REST", + "linkedRelationshipId" : "92" + } ], + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "54" + }, { + "id" : "130", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.9a758bd9-e31b-45b4-94d3-cdbfaa9be8d6.visor_ui_" + }, + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "8" + }, { + "id" : "129", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.9a758bd9-e31b-45b4-94d3-cdbfaa9be8d6.portal_ui_" + }, + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "50" + } ] + }, { + "id" : "113", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.5b403c07-dfd5-443f-a6a8-62ba6421ad57" + }, + "name" : "Orchestrator", + "description" : "the python module that starts and shutsdown the GLOW solution", + "environment" : "End User Windows Desktop PC", + "technology" : "Python", + "instances" : "1", + "softwareSystemInstances" : [ { + "id" : "123", + "tags" : "Software System Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.5b403c07-dfd5-443f-a6a8-62ba6421ad57.product_instance_manager_" + }, + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "softwareSystemId" : "51" + } ], + "containerInstances" : [ { + "id" : "115", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.5b403c07-dfd5-443f-a6a8-62ba6421ad57.dash_" + }, + "relationships" : [ { + "id" : "116", + "sourceId" : "115", + "destinationId" : "114", + "description" : "Call", + "technology" : "REST", + "linkedRelationshipId" : "93" + } ], + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "58" + }, { + "id" : "114", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.5b403c07-dfd5-443f-a6a8-62ba6421ad57.api_" + }, + "relationships" : [ { + "id" : "125", + "sourceId" : "114", + "destinationId" : "123", + "description" : "requests product termination (on shutdown)", + "technology" : "gRPC", + "linkedRelationshipId" : "83" + }, { + "id" : "120", + "sourceId" : "114", + "destinationId" : "117", + "description" : "starts & stops", + "linkedRelationshipId" : "77" + }, { + "id" : "121", + "sourceId" : "114", + "destinationId" : "117", + "description" : "starts", + "linkedRelationshipId" : "101" + }, { + "id" : "134", + "sourceId" : "114", + "destinationId" : "133", + "description" : "reads and writes project files", + "linkedRelationshipId" : "85" + }, { + "id" : "140", + "sourceId" : "114", + "destinationId" : "139", + "description" : "gets, modifies & creates records in", + "linkedRelationshipId" : "84" + } ], + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "56" + }, { + "id" : "117", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.5b403c07-dfd5-443f-a6a8-62ba6421ad57.method_process_" + }, + "relationships" : [ { + "id" : "119", + "sourceId" : "117", + "destinationId" : "114", + "description" : "uploads and downloads fields", + "technology" : "REST", + "linkedRelationshipId" : "98" + }, { + "id" : "124", + "sourceId" : "117", + "destinationId" : "123", + "description" : "queries product connection, requests product start & termination", + "technology" : "gRPC", + "linkedRelationshipId" : "79" + }, { + "id" : "136", + "sourceId" : "117", + "destinationId" : "135", + "description" : "creates & deletes", + "linkedRelationshipId" : "82" + }, { + "id" : "118", + "sourceId" : "117", + "destinationId" : "114", + "description" : "calls", + "technology" : "REST", + "linkedRelationshipId" : "80" + }, { + "id" : "137", + "sourceId" : "117", + "destinationId" : "135", + "description" : "creates and deletes", + "linkedRelationshipId" : "100" + } ], + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "69" + }, { + "id" : "122", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "end_user_windows_pc_.d67fbece-ba58-4e45-b92f-b0fedafe32f2.5b403c07-dfd5-443f-a6a8-62ba6421ad57.portal_server_" + }, + "environment" : "End User Windows Desktop PC", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "49" + } ] + } ] + } ], + "properties" : { + "structurizr.groupSeparator" : "/" + } + }, + "documentation" : { }, + "views" : { + "systemLandscapeViews" : [ { + "key" : "SystemLandscape", + "order" : 1, + "description" : "VISOR integrated in Solution Application using GLOW", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "enterpriseBoundaryVisible" : true, + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "2", + "x" : 0, + "y" : 0 + }, { + "id" : "48", + "x" : 0, + "y" : 0 + }, { + "id" : "51", + "x" : 0, + "y" : 0 + }, { + "id" : "52", + "x" : 0, + "y" : 0 + }, { + "id" : "53", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "107" + }, { + "id" : "109" + }, { + "id" : "86" + }, { + "id" : "111" + }, { + "id" : "104" + }, { + "id" : "103" + }, { + "id" : "106" + }, { + "id" : "105" + } ] + } ], + "systemContextViews" : [ { + "key" : "VisorSystemContext", + "order" : 4, + "description" : "VISOR Context", + "softwareSystemId" : "2", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "enterpriseBoundaryVisible" : true, + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "2", + "x" : 0, + "y" : 0 + }, { + "id" : "51", + "x" : 0, + "y" : 0 + }, { + "id" : "53", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "107" + }, { + "id" : "111" + }, { + "id" : "106" + }, { + "id" : "105" + } ] + }, { + "key" : "VisorSolutionApplicationContext", + "order" : 2, + "description" : "VISOR Solution Application Context", + "softwareSystemId" : "2", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "enterpriseBoundaryVisible" : true, + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "2", + "x" : 0, + "y" : 0 + }, { + "id" : "48", + "x" : 0, + "y" : 0 + }, { + "id" : "51", + "x" : 0, + "y" : 0 + }, { + "id" : "53", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "107" + }, { + "id" : "111" + }, { + "id" : "104" + }, { + "id" : "103" + }, { + "id" : "106" + }, { + "id" : "105" + } ] + } ], + "containerViews" : [ { + "key" : "VisorContainers", + "order" : 3, + "description" : "VISOR Containers", + "softwareSystemId" : "2", + "paperSize" : "A5_Landscape", + "dimensions" : { + "width" : 2480, + "height" : 1748 + }, + "externalSoftwareSystemBoundariesVisible" : false, + "elements" : [ { + "id" : "1", + "x" : 60, + "y" : 410 + }, { + "id" : "15", + "x" : 710, + "y" : 955 + }, { + "id" : "8", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "44" + }, { + "id" : "45" + }, { + "id" : "46" + }, { + "id" : "47" + } ] + } ], + "componentViews" : [ { + "key" : "VisorClientComponents", + "order" : 7, + "description" : "VISOR Client Component", + "dimensions" : { + "width" : 890, + "height" : 3211 + }, + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : true + }, + "containerId" : "8", + "externalContainerBoundariesVisible" : false, + "elements" : [ { + "id" : "11", + "x" : 220, + "y" : 820 + }, { + "id" : "13", + "x" : 220, + "y" : 2020 + }, { + "id" : "14", + "x" : 220, + "y" : 2620 + }, { + "id" : "9", + "x" : 220, + "y" : 1420 + }, { + "id" : "10", + "x" : 220, + "y" : 220 + } ] + }, { + "key" : "VisorServerComponents", + "order" : 5, + "description" : "VISOR Server Components", + "dimensions" : { + "width" : 3120, + "height" : 811 + }, + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : true + }, + "containerId" : "15", + "externalContainerBoundariesVisible" : false, + "elements" : [ { + "id" : "16", + "x" : 2450, + "y" : 220 + }, { + "id" : "17", + "x" : 950, + "y" : 220 + }, { + "id" : "18", + "x" : 1700, + "y" : 220 + }, { + "id" : "19", + "x" : 200, + "y" : 220 + } ], + "relationships" : [ { + "id" : "26" + }, { + "id" : "36" + }, { + "id" : "41" + } ] + }, { + "key" : "VisorAppComponents", + "order" : 8, + "description" : "VISOR Application Components", + "dimensions" : { + "width" : 2390, + "height" : 1411 + }, + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : true + }, + "containerId" : "19", + "externalContainerBoundariesVisible" : false, + "elements" : [ { + "id" : "22", + "x" : 1719, + "y" : 819 + }, { + "id" : "23", + "x" : 1719, + "y" : 219 + }, { + "id" : "24", + "x" : 219, + "y" : 819 + }, { + "id" : "20", + "x" : 219, + "y" : 219 + }, { + "id" : "21", + "x" : 969, + "y" : 519 + } ], + "relationships" : [ { + "id" : "29" + }, { + "id" : "28" + }, { + "id" : "31" + }, { + "id" : "30" + } ] + }, { + "key" : "VisorDashComponents", + "order" : 6, + "description" : "VISOR Dash UI Components", + "dimensions" : { + "width" : 2320, + "height" : 2011 + }, + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : true + }, + "containerId" : "3", + "externalContainerBoundariesVisible" : false, + "elements" : [ { + "id" : "1", + "x" : 200, + "y" : 470 + }, { + "id" : "4", + "x" : 900, + "y" : 220 + }, { + "id" : "5", + "x" : 900, + "y" : 820 + }, { + "id" : "6", + "x" : 900, + "y" : 1420 + }, { + "id" : "7", + "x" : 1650, + "y" : 220 + } ], + "relationships" : [ { + "id" : "27" + }, { + "id" : "42" + }, { + "id" : "43" + } ] + } ], + "deploymentViews" : [ { + "key" : "EndUserDeployment", + "order" : 9, + "description" : "End User Deployment", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "environment" : "End User Windows Desktop PC", + "elements" : [ { + "id" : "130", + "x" : 0, + "y" : 0 + }, { + "id" : "131", + "x" : 0, + "y" : 0 + }, { + "id" : "132", + "x" : 0, + "y" : 0 + }, { + "id" : "122", + "x" : 0, + "y" : 0 + }, { + "id" : "133", + "x" : 0, + "y" : 0 + }, { + "id" : "123", + "x" : 0, + "y" : 0 + }, { + "id" : "112", + "x" : 0, + "y" : 0 + }, { + "id" : "113", + "x" : 0, + "y" : 0 + }, { + "id" : "135", + "x" : 0, + "y" : 0 + }, { + "id" : "114", + "x" : 0, + "y" : 0 + }, { + "id" : "126", + "x" : 0, + "y" : 0 + }, { + "id" : "115", + "x" : 0, + "y" : 0 + }, { + "id" : "127", + "x" : 0, + "y" : 0 + }, { + "id" : "138", + "x" : 0, + "y" : 0 + }, { + "id" : "117", + "x" : 0, + "y" : 0 + }, { + "id" : "139", + "x" : 0, + "y" : 0 + }, { + "id" : "129", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "119" + }, { + "id" : "118" + }, { + "id" : "120" + }, { + "id" : "140" + }, { + "id" : "134" + }, { + "id" : "121" + }, { + "id" : "124" + }, { + "id" : "128" + }, { + "id" : "125" + }, { + "id" : "136" + }, { + "id" : "116" + }, { + "id" : "137" + } ] + } ], + "configuration" : { + "branding" : { + "logo" : "https://raw.githubusercontent.com/RVR06/cornifer-contrib/main/assets/ansys.png" + }, + "styles" : { }, + "themes" : [ "https://raw.githubusercontent.com/RVR06/cornifer-contrib/main/themes/semantic/theme.json", "https://raw.githubusercontent.com/RVR06/cornifer-contrib/main/themes/heraldry/theme.json" ], + "terminology" : { }, + "metadataSymbols" : "SquareBrackets", + "lastSavedView" : "VisorAppComponents" + } + } +} \ No newline at end of file diff --git a/doc/developer_docs/images/11_jupyter_notebook_1.png b/doc/developer_docs/images/11_jupyter_notebook_1.png new file mode 100644 index 00000000..a63d5e9c Binary files /dev/null and b/doc/developer_docs/images/11_jupyter_notebook_1.png differ diff --git a/doc/developer_docs/images/11_jupyter_notebook_2.png b/doc/developer_docs/images/11_jupyter_notebook_2.png new file mode 100644 index 00000000..e0182383 Binary files /dev/null and b/doc/developer_docs/images/11_jupyter_notebook_2.png differ diff --git a/doc/developer_docs/images/env_vars.png b/doc/developer_docs/images/env_vars.png new file mode 100644 index 00000000..a5cc7214 Binary files /dev/null and b/doc/developer_docs/images/env_vars.png differ diff --git a/doc/developer_docs/images/system_env_vars.png b/doc/developer_docs/images/system_env_vars.png new file mode 100644 index 00000000..3bff53d0 Binary files /dev/null and b/doc/developer_docs/images/system_env_vars.png differ diff --git a/doc/developer_docs/images/visor-state-model.png b/doc/developer_docs/images/visor-state-model.png new file mode 100644 index 00000000..25b5932a Binary files /dev/null and b/doc/developer_docs/images/visor-state-model.png differ diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 00000000..c4e8d933 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,53 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=_build + +if "%1" == "" goto help +if "%1" == "pdf" goto pdf +if "%1" == "clean" goto clean + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:clean +rmdir /s /q %BUILDDIR% > /NUL 2>&1 +for /d /r %SOURCEDIR% %%d in (_autosummary) do @if exist "%%d" rmdir /s /q "%%d" +for /d /r %SOURCEDIR% %%d in (examples) do @if exist "%%d" rmdir /s /q "%%d" +goto end + +:pdf +%SPHINXBUILD% -M latex %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +cd "%BUILDDIR%\latex" +for %%f in (*.tex) do ( +pdflatex "%%f" --interaction=nonstopmode) +if NOT EXIST ansys-visor.pdf ( + Echo "no pdf generated!" + exit /b 1) +Echo "pdf generated!" + +:end +popd diff --git a/doc/scripts/generate_openapi.py b/doc/scripts/generate_openapi.py new file mode 100644 index 00000000..3949300d --- /dev/null +++ b/doc/scripts/generate_openapi.py @@ -0,0 +1,6 @@ +import json + +from ansys.visor.viewer.api.server import app + +with open("source/http_api_reference/openapi.json", "w+") as f: + json.dump(app.openapi(), f, indent=2) diff --git a/doc/source/_static/README.md b/doc/source/_static/README.md new file mode 100644 index 00000000..d2c955cc --- /dev/null +++ b/doc/source/_static/README.md @@ -0,0 +1 @@ +Static files are found here (like images and other assets). diff --git a/doc/source/_static/ansys-solutions-logo-black-background.png b/doc/source/_static/ansys-solutions-logo-black-background.png new file mode 100644 index 00000000..656b6353 Binary files /dev/null and b/doc/source/_static/ansys-solutions-logo-black-background.png differ diff --git a/doc/source/_static/css/reset.css b/doc/source/_static/css/reset.css new file mode 100644 index 00000000..0b7677a8 --- /dev/null +++ b/doc/source/_static/css/reset.css @@ -0,0 +1,273 @@ +.reset { + *, + *::before, + *::after { + margin: 0; + padding: 0; + line-height: 1; + text-align: left; + font-size: inherit; + box-sizing: border-box; + border-style: solid; + border-width: 0; + visibility: inherit; + } + + .float-container-left, + .float-container-right { + + &::before, + &::after { + /*clearfix*/ + content: ""; + display: block; + clear: both; + } + + & > * { + display: block; + position: relative; + width: auto; + /*include min-height:1px because floating + divs that take up no space are removed from + the document, and we don't want them to be + removed.*/ + min-height: 1px; + } + } + + .float-container-left > * { + float: left; + } + + .float-container-right > * { + float: right; + } + + .bold { + font-weight: bold; + } + + .hard-wrap { + white-space: normal; + word-break: break-all; + } + + .no-wrap { + white-space: nowrap; + } + + .text-right { + text-align: right; + } + + .text-left { + text-align: left; + } + + .text-center { + text-align: center; + } + + *, + *:active, + *:link, + *:visited, + *:hover { + &.no-decoration { + text-decoration: none; + } + } + + .no-select { + user-select: none; + } + + .nowrap { + white-space: nowrap; + } + + .border-all { + border-width: 1px; + } + + + .margin-top { + margin-top: 10px; + } + + .margin-top-5 { + margin-top: 5px; + } + + .margin-right { + margin-right: 10px; + } + + .margin-right-5 { + margin-right: 5px; + } + + .padding-all { + padding: 10px; + } + + .padding-right { + padding-right: 10px; + } + + .padding-right-20 { + padding-right: 20px; + } + + .padding-bottom { + padding-bottom: 10px; + } + + :is(th,td).stretch { + width: 99.99%; + } + + :is(table,th,td).shrink { + width: 0.01px; + } + + .block { + display: block; + } + + .inline-block { + display: inline-block; + } + + div { + display: block; + } + + .right { + margin-left: auto; + } + + .center { + margin-left: auto; + margin-right: auto; + } + + .left { + margin-right: auto; + } + + label { + display: block; + + & > *:first-child { + &:not(input,textarea,select,button) { + font-size: 10px; + } + } + + & > *:not(:first-child) { + margin-top: 5px; + } + + & *:is(input,textarea,select,button) { + display: block; + } + } + + table.list > * > tr > *:first-child { + white-space: nowrap; + text-align: right; + width: 0.01px; + } + + table:is(.pad-h-5,.pad-h-10) { + & > * > tr > *:first-child { + padding-left: 0; + } + + & > * > tr > *:last-child { + padding-right: 0; + } + } + + table { + &.pad-h-5 > * > tr > * { + padding-left: 3px; + padding-right: 2px; + } + + &.pad-h-10 > * > tr > * { + padding-left: 5px; + padding-right: 5px; + } + } + + table:is(.pad-v-5,.pad-v-10) { + & > * > tr:first-child > * { + padding-top: 0; + } + + & > * > tr:last-child > * { + padding-bottom: 0; + } + } + + table { + &.pad-v-5 > * > tr > * { + padding-top: 3px; + padding-bottom: 2px; + } + + &.pad-v-10 > * > tr > * { + padding-top: 5px; + padding-bottom: 5px; + } + } + + table { + width: 100%; + empty-cells: show; + border-collapse: collapse; + border-spacing: 0; + table-layout: auto; + vertical-align: baseline; + display: table; + } + + tr { + display: table-row; + } + + th { + display: table-cell; + vertical-align: middle; + } + + td { + display: table-cell; + vertical-align: middle; + } + + table:is(.padh5,.padh10) { + & > * > tr > *:first-child { + padding-left: 0; + } + + & > * > tr > *:last-child { + padding-right: 0; + } + } + + table { + &.padh5 > * > tr > * { + padding-left: 3px; + padding-right: 2px; + } + + &.padh10 > * > tr > * { + padding-left: 5px; + padding-right: 5px; + } + } +} \ No newline at end of file diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css new file mode 100644 index 00000000..b264c90d --- /dev/null +++ b/doc/source/_static/custom.css @@ -0,0 +1,9 @@ +/* Box around the whole tab enclosure with a muted blue-gray border and more obvious grey background */ +.sd-tab-set { + border: 3px solid #b0b8c1; + border-radius: 10px; + padding: 1.2em 1em 1em 1em; + background: #eceff1; + box-shadow: 0 2px 12px rgba(108,122,137,0.07); + margin-bottom: 2em; +} \ No newline at end of file diff --git a/doc/source/_static/solutions_logo_dark.png b/doc/source/_static/solutions_logo_dark.png new file mode 100644 index 00000000..a01a39b4 Binary files /dev/null and b/doc/source/_static/solutions_logo_dark.png differ diff --git a/doc/source/_static/solutions_logo_light.png b/doc/source/_static/solutions_logo_light.png new file mode 100644 index 00000000..654a381a Binary files /dev/null and b/doc/source/_static/solutions_logo_light.png differ diff --git a/doc/source/_static/visor_multiblock_opacity.png b/doc/source/_static/visor_multiblock_opacity.png new file mode 100644 index 00000000..b0c75d95 Binary files /dev/null and b/doc/source/_static/visor_multiblock_opacity.png differ diff --git a/doc/source/_static/visor_standalone_many_blocks_localhost_8081_2025-07030.png b/doc/source/_static/visor_standalone_many_blocks_localhost_8081_2025-07030.png new file mode 100644 index 00000000..60b4bebf Binary files /dev/null and b/doc/source/_static/visor_standalone_many_blocks_localhost_8081_2025-07030.png differ diff --git a/doc/source/_templates/README.md b/doc/source/_templates/README.md new file mode 100644 index 00000000..86a233ca --- /dev/null +++ b/doc/source/_templates/README.md @@ -0,0 +1 @@ +## Contains templates for the documentation build diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst new file mode 100644 index 00000000..cb1b8c0b --- /dev/null +++ b/doc/source/changelog.rst @@ -0,0 +1,13 @@ +.. _ref_release_notes: + +Release notes +############# + +This document contains the release notes for the project. + +.. vale off + +.. towncrier release notes start + + +.. vale on \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 00000000..7be3e345 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,258 @@ +"""Sphinx documentation configuration file.""" + +import base64 +import os +import runpy +from datetime import datetime + +import requests +from ansys_sphinx_theme import ansys_favicon, get_version_match +from sphinx_gallery.sorting import FileNameSortKey + +from ansys.visor.viewer import __version__ + +# TODO: Set up namespace for VISOR docs and update here +# visor_cname = "visor.docs.solutions.ansys.com" +visor_cname = "vigilant-lamp-162kw9z.pages.github.io" + +cname = os.getenv("DOCUMENTATION_CNAME", visor_cname) +"""The canonical name of the webpage hosting the documentation.""" + +# Project information +project = "ansys-visor-viewer" +copyright = f"(c) {datetime.now().year} ANSYS, Inc. All rights reserved" +author = "Synopsys Inc. and Ansys Inc." +version = __version__ +__ansys_version__ = 251 + +rst_prolog = f""" +.. _Layout Templates: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_templates.html +.. _Columns: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_columns.html +.. _Panel: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_panel.html +.. _Boxes: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_boxes.html +.. _Tabs: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_tabs.html +.. _Carousel: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_carousel.html +.. _Slider: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_slider.html +.. _Page Footer: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_page_footer.html +.. _Page Header: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_page_header.html +.. _Iterator: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_iterator.html +.. _Tag to Properties: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_tag_properties.html +.. _Table of Contents: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_table_of_contents.html +.. _Link Report: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_linked_report.html +.. _Table Merge: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_table_merge.html +.. _Table Reduction: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_table_reduction.html +.. _Table Row/Column Filter: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_table_row_column_filter.html +.. _Table Value Filter: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_table_value_filter.html +.. _Table Row/Column Sort: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_table_row_column_sort.html +.. _SQL Query: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_sql_query.html +.. _Tree Merge: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_tree_merge.html +.. _Userdefined: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_layout_user_defined_block.html +.. _Generator templates: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_generator_templates.html +.. _Statistical Analysis: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/ad_ug_generator_statistical_analysis.html +.. _Table: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_data_item_table.html +.. _Query Expressions: https://ansyshelp.ansys.com/public/account/secured?returnurl=Views/Secured/corp/v{__ansys_version__}/en/adr_ug/adr_ug_query_expressions.html + +""" + +# Select desired logo, theme, and declare the html title +html_logo = "_static/solutions_logo_light.png" +html_theme = "ansys_sphinx_theme" +html_short_title = html_title = "VISOR documentation |version|" +switcher_version = get_version_match(version) +html_favicon = ansys_favicon + +# specify the location of your github repo +html_context = { + "github_user": "ansys", + "github_repo": "visor", + "github_version": "main", + "doc_path": "doc/source", +} + +# specify the location of your github repo +# Note: if the visor repo becomes public, we can set +# "json_url": f"https://{cname}/versions.json" +# and remove fetch_and_save_versions_json() +html_theme_options = { + "switcher": { + "json_url": "_static/versions.json", + "version_match": switcher_version, + }, + "github_url": "https://github.com/ansys/visor/", + "show_prev_next": False, + "show_breadcrumbs": True, + "collapse_navigation": True, + "use_edit_page_button": True, + "logo": { + "image_light": "_static/solutions_logo_light.png", + "image_dark": "_static/solutions_logo_dark.png", + }, +} + +# Sphinx extensions +extensions = [ + # "sphinx.ext.napoleon", # Use this if you want to use Google style docstrings + "numpydoc", + "sphinx.ext.autodoc", + 'sphinx_autodoc_typehints', + "sphinxcontrib.openapi", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx_copybutton", + "sphinx_gallery.gen_gallery", + "sphinx_design", +] + +autoapi_options = [ + "members", + "undoc-members", + "private-members", + "special-members", + "show-inheritance", + "show-module-summary", + "imported-members", +] + +# Intersphinx mapping +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + # kept here as an example + # "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + # "numpy": ("https://numpy.org/devdocs", None), + # "matplotlib": ("https://matplotlib.org/stable", None), + # "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + # "pyvista": ("https://docs.pyvista.org/", None), +} + +# numpydoc configuration +numpydoc_show_class_members = False +numpydoc_xref_param_type = True + +# Consider enabling numpydoc validation. See: +# https://numpydoc.readthedocs.io/en/latest/validation.html# +numpydoc_validate = True +numpydoc_validation_checks = { + "GL06", # Found unknown section + "GL07", # Sections are in the wrong order. + # "GL08", # The object does not have a docstring + "GL09", # Deprecation warning should precede extended summary + "GL10", # reST directives {directives} must be followed by two colons + # "SS01", # No summary found + "SS02", # Summary does not start with a capital letter + # "SS03", # Summary does not end with a period + "SS04", # Summary contains heading whitespaces + # "SS05", # Summary must start with infinitive verb, not third person + "RT02", # The first line of the Returns section should contain only the + # type, unless multiple values are being returned" +} + +# -- Sphinx Gallery Options +examples_source = os.path.join(os.path.dirname(__file__), "examples_source") + +sphinx_gallery_conf = { + # convert rst to md for ipynb + "pypandoc": False, + # path to your examples scripts + "examples_dirs": [ + examples_source, + ], + # path where to save gallery generated examples + "gallery_dirs": [ + "examples", + ], + # Pattern to search for example files + "filename_pattern": r"\.py", + # Remove the "Download all examples" button from the top level gallery + "download_all_examples": False, + # Sort gallery example by file name instead of number of lines (default) + "within_subsection_order": FileNameSortKey, + # directory where function granular galleries are stored + "backreferences_dir": None, + # the initial notebook cell + "first_notebook_cell": ("# ``visor`` example Notebook\n" "#\n"), + "plot_gallery": False, +} + +# static path +html_static_path = ["_static"] + +# CSS files +html_css_files = ['custom.css'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + + +# debugging segfault when running the seupt script +import faulthandler + +faulthandler.enable() + + +def fetch_and_save_versions_json(): + """ + Fetches the `versions.json` file from the `gh-pages` branch of the private + VISOR repository using the GitHub API and saves it locally to + `source/_static/versions.json`. + + This is required for the version switcher, as the repository is private and + the file cannot be accessed via the GitHub Pages URL without authentication. + + Requires a valid `GITHUB_TOKEN` for authentication. + This is automatically set in the GitHub Actions workflow, but must be set + manually for local builds, e.g.: + export GITHUB_TOKEN="your_tokem_here" + """ + owner = "ansys" + repo = "visor" + branch = "gh-pages" + file_path = "versions.json" + api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/{file_path}?ref={branch}" + token = os.getenv("GITHUB_TOKEN") + headers = {"Authorization": f"Bearer {token}"} if token else {} + + local_path = os.path.join("source", "_static", "versions.json") + print(f"Fetching {file_path} from {repo}@{branch} via GitHub API...") + + try: + response = requests.get(api_url, headers=headers) + response.raise_for_status() + content = response.json()["content"] + decoded = base64.b64decode(content).decode("utf-8") + with open(local_path, "w+", encoding="utf-8") as f: + f.write(decoded) + print(f"Saved versions.json to {local_path}") + except Exception as e: + print(f"Error fetching versions.json: {e}") + + +# Run the script to generate an updated OpenAPI JSON file + +def generate_openapi_json(): + try: + runpy.run_path('scripts/generate_openapi.py') + except Exception as e: + print(f"Error running generate_openapi.py: {e}") + +def setup(app): + app.connect('builder-inited', lambda app: generate_openapi_json()) + app.connect('builder-inited', lambda app: fetch_and_save_versions_json()) + app.add_css_file("css/reset.css") + +linkcheck_ignore = [] + +# If we are on a release, we have to ignore the "release" URLs, since it is not +# available until the release is published. +if switcher_version != "dev": + linkcheck_ignore.append( + f"https://github.com/ansys/visor/releases/tag/v{__version__}" + ) \ No newline at end of file diff --git a/doc/source/contributing/index.rst b/doc/source/contributing/index.rst new file mode 100644 index 00000000..faef3c51 --- /dev/null +++ b/doc/source/contributing/index.rst @@ -0,0 +1,349 @@ +.. _contributing-index: + +Contributing to VISOR +##################### + +Use this guide to contribute to ``VISOR``. + +Ansys, part of Synopsys, maintains ``VISOR`` and reviews every submission before merging. +You can also help other users, answer questions, and contribute features that make the software more useful. + +Before you contribute to ``VISOR``, read the +`Contributing <https://dev.docs.pyansys.com/how-to/contributing.html>`_ topic +in the **PyAnsys developer's guide**. + + +Clone the repository +==================== + +Follow the steps in the installation section of the :ref:`installation-index` to set up VISOR in development mode. + + +Commit message guidelines +========================= + +This project follows the `Conventional Commits <https://www.conventionalcommits.org>`_ specification. + +Commit format +------------- + +Use these prefixes in your commit messages: + +- Use ``feat:`` for a new feature. +- Use ``fix:`` for a bug fix. +- Use ``docs:`` for documentation updates only. +- Use ``chore:`` for maintenance tasks that do not affect production code. +- Use ``refactor:`` for code restructuring that does not change behavior. +- Use ``style:`` for formatting or style changes that do not affect logic. +- Use ``test:`` for adding or modifying tests. +- Use ``ci:`` for CI configuration changes. +- Use ``build:`` for build script or dependency changes. + +Branch naming conventions +------------------------- + +Use these branch name prefixes: + +- Use ``fix`` for minor bug fixes, patches, or experiments. +- Use ``feat`` for a new feature or significant addition. +- Use ``junk`` for experimental changes that you can delete if they go stale. +- Use ``maint`` for general repository or CI maintenance. +- Use ``doc`` for documentation-only changes. +- Use ``no-ci`` for low-impact work that should not trigger CI. +- Use ``testing`` for test improvements or test-related changes. +- Use ``release`` for release work. + + +Run tests +========= + +Install the development packages that you need to run tests: + +.. code-block:: bash + + poetry install --with dev + + + +Run tests for each layer +------------------------ + +Use the following test layers in the ``tests/`` directory: + +.. list-table:: + :header-rows: 1 + + * - Layer + - Location + - Description + * - Unit + - ``tests/unit/`` + - Fast, isolated component tests + * - Frontend unit + - ``src/ansys/visor/visor-client/`` + - TypeScript/React component tests (Jest) + * - Integration + - ``tests/integration/`` + - Multi-component interaction tests + * - Smoke + - ``tests/e2e/smoke/`` + - WebGL render and interaction sanity checks (Playwright) + * - Regression + - ``tests/e2e/regressions/`` + - Visual snapshot and component state validation (Playwright) + * - SAF integration + - ``tests/e2e/smoke/test_smoke_saf_visor*.py`` + - ``saf``-marked smoke tests against a real ``saf-visor-poc`` solution (Playwright) + * - Notebooks + - ``tests/notebooks/`` + - Jupyter + + +.. note:: + When you run ``pytest`` from the project root, it discovers only ``unit`` and ``integration`` tests by default. + Run smoke, regression, and Notebook tests explicitly, as shown in the next sections. + +Run unit and integration tests: + +.. code-block:: bash + + # Unit and integration (default discovery) + poetry run pytest tests/unit tests/integration + + # With verbose output + poetry run pytest tests/unit tests/integration -v + + # Filter by marker + poetry run pytest tests -m "unit" + poetry run pytest tests -m "not e2e" + + # With coverage report + poetry run pytest tests/unit tests/integration --cov=src --cov-report=html + + +Run end-to-end tests (Playwright) +--------------------------------- + +Install Playwright before you run end-to-end (e2e) tests: + +.. code-block:: bash + + poetry run playwright install chromium + + +Run smoke and regression tests: + +.. code-block:: bash + + poetry run pytest tests/e2e/smoke + poetry run pytest tests/e2e/regressions + + +In **Linux headless environments**, e2e tests require a virtual display. Set these variables before you run e2e tests: + +.. code-block:: bash + + Xvfb :99 -screen 0 1920x1080x24 & + export DISPLAY=:99 + export VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 + + +If a test fails, Playwright saves traces as ZIP files in the ``tests/artifacts/traces/`` directory. + + +Run SAF integration smoke tests +------------------------------- + +``tests/e2e/smoke/test_smoke_saf_visor.py`` verify that ``saf-visor-poc`` correctly loads and visualizes +VTK files through VISOR when run as a SAF/Glow solution. +These tests are marked ``saf`` and are skipped automatically when the +``saf`` CLI is not available on ``PATH``. + +They also require a local clone of ``saf-visor-poc``, pointed to via the ``--saf-project-dir`` +option or the ``SAF_VISOR_POC_DIR`` environment variable: + +.. code-block:: bash + + poetry run pytest tests/e2e/smoke -m saf --saf-project-dir "/path/to/saf-visor-poc" + + # or, using the environment variable + export SAF_VISOR_POC_DIR="/path/to/saf-visor-poc" + poetry run pytest tests/e2e/smoke -m saf + + +The test launches its own ``saf run`` subprocess (module-scoped fixture) against a fresh +port pair and tears it down afterward. Notes: + +- SAF server startup (including SAF/Glow log noise and the VISOR launch alert) can take up to + ~2 minutes; do not lower the fixture's timeouts without reproducing locally first. +- The test retries the file upload a few times if it hits the transient + "shared product instance ... has not been initialized" GLOW error, which can occur briefly + after the VISOR launch alert appears. +- If tests hang or fail with a ``500`` error or a launch-alert timeout, check for orphaned + ``saf``/``dotnet`` (PIM Light Server) processes left over from a previous interrupted run + and stop them before retrying. + + +Run Notebook tests +------------------ + +.. code-block:: bash + + poetry run pytest --nbval tests/notebooks/ + + +Run frontend unit tests (Jest) +------------------------------ + +Run the TypeScript/React frontend test suite with Jest. You do not need the Python environment for these tests. + +.. code-block:: bash + + cd src/ansys/visor/visor-client + npm run test + + +To generate a JUnit XML report for CI: + +.. code-block:: bash + + JEST_JUNIT_OUTPUT_DIR=../../../../tests/artifacts/unit \ + JEST_JUNIT_OUTPUT_NAME=frontend-junit.xml \ + npx jest --ci --reporters=default --reporters=jest-junit --verbose + + +Jest writes report files to the ``tests/artifacts/unit/`` directory alongside the Python unit test reports: + +- ``frontend-junit.xml``: JUnit XML result file +- ``frontend-result.log``: Console output log + +Set testing options +------------------- + +Update canonicals +~~~~~~~~~~~~~~~~~ + +If you change UI elements or example models, smoke and regression tests that use image comparison might fail. +Update the canonical images either by replacing the baseline image manually or by running ``pytest`` with the +``--update-baseline`` option: + +.. code-block:: bash + + poetry run pytest ./tests/e2e/smoke --update-baseline + poetry run pytest ./tests/e2e/regressions --update-baseline + + +If no canonical image is found, ``pytest`` uses this option by default and skips the test on the first run. + + +Use a different location for canon images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, canonical images are stored in the ``tests/references`` directory. Use the ``--baseline-dir`` option to +change this path: + +.. code-block:: bash + + poetry run pytest ./tests/e2e/smoke --baseline-dir "tests/new_ref_loc" + + +If the directory does not exist, ``pytest`` creates it and generates a new baseline image on the first run. + + +Set a different pixel threshold +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you create new tests for visual elements, you might need to adjust the pixel threshold. +The RMS difference is usually 0, but some models or mappings can introduce small session-to-session variations. +The default threshold is 2.55 RMS. + +To set a different pixel threshold: + +.. code-block:: bash + + poetry run pytest ./tests/e2e/smoke --pixel-threshold 5 + + +This does not update the default value. + +Use a different host +~~~~~~~~~~~~~~~~~~~~ + +By default, ``VISOR`` launches on ``127.0.0.1``. To test against a different host, use the ``--host`` option: + +.. code-block:: bash + + poetry run pytest ./tests/e2e/smoke --host 0.0.0.0 + + +Build and view documentation locally +==================================== + +Sphinx generates the ``VISOR`` documentation. The source files are in the ``doc`` directory. +The ``.github/workflows/nightly-docs.yml`` workflow rebuilds the documentation nightly. + +Build documentation +------------------- + +To build the documentation locally, perform the following steps: + +.. code-block:: bash + + # navigate to the docs directory + cd docs + + # Install the documentation dependencies + poetry install --with doc + + # Build the documentation + # on Windows: + make.bat html + # on Linux: + make html + + +After the build completes, open the ``index.html`` file in the ``_build/html`` directory in a web browser. + +For example, if you cloned the ``VISOR`` repository to ``file:///C:/SYNOPSYSDev/NoBackup/visor``, open +the ``file:///C:/SYNOPSYSDev/NoBackup/visor/doc/_build/html/index.html`` file in a web browser. + + +Set up the documentation version switcher (optional) +---------------------------------------------------- + +The documentation version switcher lets you toggle between documentation versions. The ``doc/source/conf.py`` file +configures it. During the release process, the build generates a ``versions.json`` file and commits it to the +``gh-pages`` branch, which is not present in ``main``. + +The version switcher works only on the live documentation site or when you serve the documentation from a +local web server. If you open ``index.html`` directly from the filesystem, the version switcher is disabled. + +To enable the version switcher while serving the documentation locally, follow these steps: + +1. Create a GitHub Personal Access Token (PAT). + +The version switcher fetches the ``versions.json`` file from the private VISOR repository, +which requires authentication. + +- Go to `Personal access tokens (classic) <https://github.com/settings/tokens>`_ in the GitHub developer settings. +- Click **Generate new token** and select the **Generate new token (classic)** option. +- Check the ``repo`` scope only. +- Click **Generate token** and copy the value. +- Activate the token for use in the Ansys and Ansys-internal organizations. +- Set the token as an environment variable named ``GITHUB_TOKEN``. + + +Note: The documentation deployment GitHub workflow generates a PAT automatically. + + +2. Serve with a local web server. + +Because of browser security restrictions, serve the documentation over HTTP for the version switcher to work: + +.. code-block:: bash + + cd doc + python -m http.server 8000 + + +Then open the `http://localhost:8000/_build/html/index.html <http://localhost:8000/_build/html/index.html>`_ file. diff --git a/doc/source/examples_source/00-basic/00-launch-visor.py b/doc/source/examples_source/00-basic/00-launch-visor.py new file mode 100644 index 00000000..00d7e44a --- /dev/null +++ b/doc/source/examples_source/00-basic/00-launch-visor.py @@ -0,0 +1,58 @@ +""" +.. _ref_launch_visor: + +Launch VISOR +============ + +To launch the VISOR visualization, instantiate the viewer and call the ``start`` method +with a file name or a ``vtkDataSet`` object. + +This example shows how to run VISOR in a Jupyter notebook. It creates a VISOR visualizer +on the default port (8081) and views it in an iframe. + +If you are not using a Jupyter notebook, the VISOR server is viewable in a new browser tab. + +Note that the VISOR server runs in a background threads, so the main thread is free to +continue executing other code. +""" + +########################################################### +from IPython.display import IFrame + +from ansys.visor.viewer import Visor + +# Define paths to two different assets +asset_path1 = '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm' +asset_path2 = '../../examples/assets/tensors9.vtp' + +# Instantiate a VISOR visualizer on port 8081. +visor = Visor(url='http://localhost:8081') + +# Start the visualizer server in the background using the first asset +visor.start(asset_path1) + +# VISOR starts a server and opens a browser window to visualize the data. +# The server runs in the background until visualizer.stop() is called or the main process is stopped. +# In a Jupyter notebook, the VISOR server stops when the kernel is restarted or stopped. + +# View the visualizer inline (if in a Jupyter Notebook) +IFrame(src='http://localhost:8081', width=1200, height=600) + +# Update the visualizer with the second asset +visor.update(asset_path2) + +# Print information about the visualizer instance +visor.info() +# {'app_name': 'VISOR Viewer', +# 'host': 'localhost', +# 'port': 8081, +# 'standalone': True, +# 'file_input_path': '../../examples/assets/tensors9.vtp', +# 'metadata': None} + +# Stop the first VISOR instance +visor.stop() + + + + diff --git a/doc/source/examples_source/00-basic/01-launch-two-visor-viewers.py b/doc/source/examples_source/00-basic/01-launch-two-visor-viewers.py new file mode 100644 index 00000000..00e1b9da --- /dev/null +++ b/doc/source/examples_source/00-basic/01-launch-two-visor-viewers.py @@ -0,0 +1,49 @@ +""" +.. _ref_launch_two_visor_viewers: + +Launch two concurrent VISOR visualizers +======================================= + +To launch the VISOR visualization, instantiate the viewer and call the ``start`` method +with a file name or a ``vtkDataSet`` object. + +This example shows how to run VISOR in a Jupyter notebook. It creates two separate +VISOR visualizers on two distinct ports (8081, 8082) and views them in iframes. + +Note that VISOR servers run in background threads, so the main thread is free to +continue executing other code. +""" + +########################################################### +from IPython.display import IFrame + +from ansys.visor.viewer import Visor + +# Define paths to two different assets +asset_path1 = '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm' +asset_path2 = '../../examples/assets/tensors9.vtp' + +# Instantiate a VISOR visualizer on port 8081. +visor1 = Visor(url='http://localhost:8081') + +# Start the visualizer server in the background using the first asset +visor1.start(asset_path1) + +# View the visualizer inline (if in a Jupyter notebook) +IFrame(src='http://localhost:8081', width=1200, height=600) + +# Update the visualizer with the second asset +visor1.update(asset_path2) + +# Initialize a second VISOR visualizer on port 8082 +visor2 = Visor(url='http://localhost:8082') + +# Start the second visualizer server in the background +visor2.start(asset_path1) +# IFrame(src='http://localhost:8082', width=1200, height=600) # Uncomment if running in a Jupyter notebook + +# Stop the first VISOR instance +visor1.stop() + +# Stop the second VISOR instance +visor2.stop() diff --git a/doc/source/examples_source/00-basic/README.txt b/doc/source/examples_source/00-basic/README.txt new file mode 100644 index 00000000..310b1b09 --- /dev/null +++ b/doc/source/examples_source/00-basic/README.txt @@ -0,0 +1,6 @@ +.. _basic-gallery: + +Basic examples using VTK files +############################## + +These basic examples show how to use VISOR to visualize an asset (VTK file). \ No newline at end of file diff --git a/doc/source/examples_source/01-basic-vtk-object-input/00-visor-vtk-input.py b/doc/source/examples_source/01-basic-vtk-object-input/00-visor-vtk-input.py new file mode 100644 index 00000000..ef66f0e2 --- /dev/null +++ b/doc/source/examples_source/01-basic-vtk-object-input/00-visor-vtk-input.py @@ -0,0 +1,50 @@ +""" +.. _ref_launch_visor_with_vtk_object: + + +Launch VISOR with a VTK object +============================== + +To launch the VISOR visualization, instantiate the viewer and call the ``start`` method +with a ``vtkDataSet`` object. + +This example shows how to create a simple cube using VTK and visualize it using VISOR. + +""" + +from IPython.display import IFrame +from vtk import vtkCubeSource + +from ansys.visor.viewer import Visor + + +# Create a cube source and get the output as a vtkPolyData object +def get_cube(x: float, y: float, z: float): + cube_source = vtkCubeSource() + cube_source.SetXLength(x) + cube_source.SetYLength(y) + cube_source.SetZLength(z) + cube_source.Update() + return cube_source.GetOutput() + + +cube = get_cube(1.0, 2.0, 3.0) +# Visualize the cube using VISOR +visualizer = Visor() +visualizer.start(input=cube) + +# VISOR starts a server and opens a browser window to visualize the data. +# The server runs in the background until visualizer.stop() is called or the main process is stopped. +# In a Jupyter notebook, the VISOR server stops when the kernel is restarted or stopped. + +# View the visualizer inline (if in a Jupyter notebook) +IFrame(src='http://localhost:8081', width=1200, height=600) + +# Update the server by passing a modified vtkDataSet object to the VISOR instance's update method +cube = get_cube(1.0, 2.0, 3.0) +visualizer.update(input=cube) + +# To stop the server, call the stop method +visualizer.stop() + + diff --git a/doc/source/examples_source/01-basic-vtk-object-input/README.txt b/doc/source/examples_source/01-basic-vtk-object-input/README.txt new file mode 100644 index 00000000..47af52e9 --- /dev/null +++ b/doc/source/examples_source/01-basic-vtk-object-input/README.txt @@ -0,0 +1,6 @@ +.. _basic-vtk-object-gallery: + +Basic examples using VTK objects +################################ + +These basic examples show how to use VISOR to visualize an asset using a VTK object. \ No newline at end of file diff --git a/doc/source/examples_source/02-specifying-per-part-state/00-metadata-per-part-opacity.py b/doc/source/examples_source/02-specifying-per-part-state/00-metadata-per-part-opacity.py new file mode 100644 index 00000000..f6119916 --- /dev/null +++ b/doc/source/examples_source/02-specifying-per-part-state/00-metadata-per-part-opacity.py @@ -0,0 +1,78 @@ +""" +.. _ref_per_part_opacity: + +Set per-part opacity for multiblock dataset +=========================================== + +Create a small VTK multiblock dataset, with unique names for each part (leaf block). + +Create a Metadata object to set custom opacity for each part of the dataset. + +Pass the dataset and metadata to VISOR.start() to visualize the dataset with the specified per-part opacity. + +.. image:: /_static/visor_multiblock_opacity.png + :alt: VISOR multiblock with per-part opacity + :width: 600px + :align: center + +""" + + +from vtk import vtkCompositeDataSet, vtkSphereSource +from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet + +from ansys.visor.viewer import Metadata, Visor + + +def make_simple_block(source_id: int) -> vtkSphereSource: + """Create a small sphere with a slightly different radius/center per block.""" + sphere = vtkSphereSource() + sphere.SetRadius(1.0) + sphere.SetCenter(source_id * 3.0, 0.0, 0.0) + sphere.SetThetaResolution(16) + sphere.SetPhiResolution(16) + sphere.Update() + return sphere.GetOutput() + +def make_multiblock() -> vtkMultiBlockDataSet: + """Create a multiblock dataset with 3 blocks.""" + + part_names = ["Part1", "Part2", "Part3"] + + multiblock = vtkMultiBlockDataSet() + + for i, name in enumerate(part_names): + block = make_simple_block(i) + multiblock.SetBlock(i, block) + # Assign a unique name to this block + multiblock.GetMetaData(i).Set(vtkCompositeDataSet.NAME(), name) + + return multiblock + + +################################################################################ +# Create the multiblock and metadata +################################################################################ +multiblock_data = make_multiblock() + +# Create a metadata dictionary specifying the opacity for each part +metadata = Metadata( + name="multiblock_example", + unit="m", + state={ + "parts": + { + "Part1": {"opacity": 0.2}, + "Part2": {"opacity": 0.5}, + "Part3": {"opacity": 0.8}, + } + } +) + +################################################################################ +# Instantiate the viewer and start it with the data and metadata we just created +################################################################################ +visualizer = Visor() +visualizer.start(input=multiblock_data, metadata=metadata) + + diff --git a/doc/source/examples_source/02-specifying-per-part-state/README.txt b/doc/source/examples_source/02-specifying-per-part-state/README.txt new file mode 100644 index 00000000..16ecd103 --- /dev/null +++ b/doc/source/examples_source/02-specifying-per-part-state/README.txt @@ -0,0 +1,6 @@ +.. _specifying-state-gallery: + +Examples specifying per-part state +################################## + +This example uses VISOR metadata to specify per-part state. diff --git a/doc/source/examples_source/03-updating-visor-scene/00-add-remove-dataset.py b/doc/source/examples_source/03-updating-visor-scene/00-add-remove-dataset.py new file mode 100644 index 00000000..5894b6e5 --- /dev/null +++ b/doc/source/examples_source/03-updating-visor-scene/00-add-remove-dataset.py @@ -0,0 +1,46 @@ +""" +.. _ref_add_remove_dataset: + + +Add and remove datasets +======================= + +You can add and remove datasets from an existing VISOR scene without restarting the visualizer. + +This example shows how to add a new dataset to an existing VISOR scene, list all +datasets in the scene, and remove an existing dataset by its ID. + +""" + +from ansys.visor.viewer import Visor + +# Set up example assets, which exist in the VISOR repository. +# File 1 + Metadata 1 +input_file1 = "examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm" +metadata_file1 = "examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json" +# File 2 + Metadata 2 +input_file2 = "examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm" +metadata_file2 = "examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json" + + +# Start VISOR visualizer with an initial dataset +visualizer = Visor() +visualizer.start(input=input_file1, metadata=metadata_file1) + +# Add a dataset to the existing scene +print(f"Adding dataset: {input_file2} with metadata: {metadata_file2}") +visualizer.add_dataset(input="new_dataset.vtu", metadata="new_metadata.json") + +# List all datasets in the current scene +datasets = visualizer.list_datasets() # returns a dictionary keyed by dataset IDs +print(f"Current datasets in the scene: {datasets}") + +# Remove one of the datasets +dataset_id = list(datasets.keys())[0] # Get the first dataset ID + +# Remove a dataset by its ID (assuming the ID is known, such as 1) +print(f"Removing dataset with ID: {dataset_id}") +visualizer.remove_dataset(dataset_id=dataset_id) + +# Stop the visualizer +visualizer.stop() \ No newline at end of file diff --git a/doc/source/examples_source/03-updating-visor-scene/01-update-variables.py b/doc/source/examples_source/03-updating-visor-scene/01-update-variables.py new file mode 100644 index 00000000..d7f0fbb0 --- /dev/null +++ b/doc/source/examples_source/03-updating-visor-scene/01-update-variables.py @@ -0,0 +1,203 @@ +""" +.. _ref_update_variables: + + +Update variables in existing datasets +===================================== + +You can update the variables of existing datasets in a VISOR scene without reloading the entire dataset. + +The ``list_variables`` and ``update_variables`` methods of the VISOR visualizer and +the corresponding APIs in the VISOR service expose this feature. + +This example shows how to list the variables of an existing dataset in a VISOR scene +and update one of the variables with new data. + +.. note:: + + This feature is only available for ``vtkUnstructuredGrid`` and ``vtkPolyData`` dataset types. + It is not supported for ``vtkMultiBlockDataSet`` or ``vtkMultiPieceDataSet`` dataset types. + +""" +########################################## +# Import modules and define helper classes +########################################## + +from typing import List + +import numpy as np +from vtk import vtkFloatArray, vtkSphereSource + +from ansys.visor.viewer import Metadata, Visor + + +class MeshCreator: + """ + Helper class to create a vtkPolyData mesh (sphere) and add variables to it. + + Parameters + ---------- + scale : float + Scale (radius) of the sphere. + xoffset : float + Offset of the sphere center along the x-axis. + + Attributes + ---------- + polydata : vtkPolyData + Generated sphere mesh. + num_points : int + Number of points in the mesh. + + """ + def __init__(self, scale=1.0, xoffset=0.0): + self.scale = scale + self.xoffset = xoffset + self.polydata = self._generate_polydata() + + @property + def num_points(self): + return self.polydata.GetNumberOfPoints() + + def add_variable(self, name, num_components, values): + vals = np.asarray(values, dtype=np.float32) + + # Enforce correct shape + if vals.shape != (self.num_points, num_components): + raise ValueError( + f"Expected shape ({self.num_points}, {num_components}), " + f"got {vals.shape}" + ) + + arr = vtkFloatArray() + arr.SetName(name) + arr.SetNumberOfComponents(num_components) + arr.SetNumberOfTuples(self.num_points) + + for i in range(self.num_points): + if num_components == 1: + arr.SetValue(i, float(vals[i, 0])) + else: + row = vals[i, :].tolist() + arr.SetTuple(i, row) + self.polydata.GetPointData().AddArray(arr) + self.polydata.GetPointData().SetActiveScalars(name) + + def add_constant_variable(self, name: str, num_components: int, constant_values: List[float]): + values = self.get_constant_values(num_components, constant_values) + self.add_variable(name, num_components, values) + + def add_random_variable(self, name: str, num_components: int, scale_factor=1.0): + values = self.get_random_values(num_components, scale_factor=scale_factor) + self.add_variable(name, num_components, values) + + def get_constant_values(self, num_components: int, constant_values: List[float]) -> np.ndarray: + """ + Create an array with a different constant value per component. + + The values list is used to set the constant value across all points for the corresponding component. + """ + if len(constant_values) != num_components: + raise ValueError("constant_values list length needs to equal num_components") + + # Numpy array of shape (self.num_points, num_components) + arr = np.empty((self.num_points, num_components), dtype=np.float32) + for i in range(0, num_components): + arr[:, i] = float(constant_values[i]) + return arr + + def get_random_values(self, num_components=1, scale_factor=1.0) -> np.ndarray: + """ + Create an array with random variables for each component, from 0 to the scale factor value. + """ + rng = np.random.default_rng() + return (scale_factor * rng.random((self.num_points, num_components), dtype=np.float32)) + + def _generate_polydata(self): + source = vtkSphereSource() + source.SetRadius(self.scale) + source.SetThetaResolution(32) + source.SetPhiResolution(32) + source.SetCenter(self.xoffset, 0, 0) + source.Update() + return source.GetOutput() + +############################### +# Set up the mesh and variables +############################### + +# Define the names and number of components for the vector variable to update +TEST_VECTOR_NAME = "test_vector" +NUM_COMPONENTS = 3 +TEST_SCALAR_NAME = "test_scalar" +SCALE_FACTOR = 5.0 + +# Create initial mesh with variables +# Set up first mesh +data_obj1 = MeshCreator(xoffset=-2) + +# Add the variables +data_obj1.add_random_variable(TEST_VECTOR_NAME, NUM_COMPONENTS, scale_factor=SCALE_FACTOR) +data_obj1.add_random_variable(TEST_SCALAR_NAME, 1, scale_factor=SCALE_FACTOR) + +# Retrieve the polydata object +polydata1 = data_obj1.polydata + +############################ +# Initialize and start VISOR +############################ + +vis = Visor() +vis.start(polydata1, metadata=Metadata(name="sphere_1", unit="m")) + +###################################### +# List datasets and get dataset the ID +###################################### + +# Print dataset metadata and get the dataset ID +datasets = vis.list_datasets() +# print(json.dumps(datasets, indent=4)) +# Get the dataset ID - there is only one loaded. +dataset_id = list(datasets.keys())[0] +print(f"dataset ID: {dataset_id}") + +################################################## +# Create new variable data and compile the payload +################################################## + +# Create the new variables for the test point dataArrays: +# a list of random values between 0 and 10 +new_vector_values = data_obj1.get_constant_values(NUM_COMPONENTS, [1.0, 2.0, 3.5]) +new_scalar_values = data_obj1.get_constant_values(1, [5.0]) + +# Compile a dict with the vector variable metadata and updated values +vector_update_info = { + "type": "point", + "name": TEST_VECTOR_NAME, + "num_components": NUM_COMPONENTS, + "data": new_vector_values +} +# Compile a dict with the scalar variable metadata and updated values +scalar_update_info = { + "type": "point", + "name": TEST_SCALAR_NAME, + "num_components": 1, + "data": new_scalar_values +} + +################################### +# Run the update variable operation +################################### + +# Run the actual update command +vis.update_variables(dataset_id, [vector_update_info, scalar_update_info]) + +# The VISOR scene should now reflect the updated variable values. + +# Stop the visualizer +vis.stop() + +################ +# End of example +################ + diff --git a/doc/source/examples_source/03-updating-visor-scene/README.txt b/doc/source/examples_source/03-updating-visor-scene/README.txt new file mode 100644 index 00000000..dc3160d2 --- /dev/null +++ b/doc/source/examples_source/03-updating-visor-scene/README.txt @@ -0,0 +1,10 @@ +.. updating-visor-scene: + +Examples updating VISOR datasets and variables +############################################## + +These examples show how to update the VISOR scene programmatically after it has been created, +for example to add and remove datasets, or to update variables in existing datasets. + + + diff --git a/doc/source/examples_source/README.txt b/doc/source/examples_source/README.txt new file mode 100644 index 00000000..5d1bf2be --- /dev/null +++ b/doc/source/examples_source/README.txt @@ -0,0 +1,7 @@ + +.. _gallery: + +Examples +-------- + +This section provides VISOR usage examples. diff --git a/doc/source/getting_started/configure_visor.rst b/doc/source/getting_started/configure_visor.rst new file mode 100644 index 00000000..71aadc3f --- /dev/null +++ b/doc/source/getting_started/configure_visor.rst @@ -0,0 +1,104 @@ +.. _configure-visor: + +VISOR configuration +################### + +Place a ``.visor`` file at the root of your project to customize runtime behavior, +including log paths, the default host and port, and UI preferences. + +With a ``.visor`` file, you can override default values in the ``Settings`` class without code changes. +VISOR loads this YAML configuration file at startup. + +When to use +=========== + +Use a ``.visor`` file for the following configuration tasks: + +- Define team-wide defaults for paths, assets, and feature flags. +- Configure different deployment environments. +- Customize application behavior without editing source code. + +VISOR applies the ``.visor`` file when you run VISOR as a service or use the Python API. + + +File name and location +====================== + +- Use ``.visor`` as the file name (YAML format). +- Place the file in the current working directory at startup. + +VISOR reads the ``.visor`` file during initialization. If the file is not present, VISOR uses built-in defaults. + + +Precedence and merge rules +========================== + +VISOR applies configuration values in the following order, from lowest to highest precedence: + +1. Built-in ``Settings`` defaults +2. Values defined in the ``.visor`` file + +Any values that you define in the ``.visor`` file override the corresponding default settings. + + +Settings defaults +================= + +You can use a ``.visor`` file to override the following default values from the +`Settings class <https://github.com/ansys-internal/theia/blob/707770cf39101281b16cfaa58ab1bee2aa1cebe2/src/ansys/theia/viewer/config.py>`_: + +.. code-block:: yaml + + app_name: "VISOR Viewer" + default_host: "localhost" + default_port: 8081 + default_standalone: False + default_dark_mode: True + default_log_dir: str(Path.cwd().joinpath("logs")) + trame_log_dir: None + binding_host: null + +Descriptions of each setting follow: + +- ``app_name``: VISOR application name to show in the title bar and window manager. +- ``default_host``: Default host for the VISOR server. +- ``default_port``: Default port for the VISOR server. +- ``default_standalone``: Whether VISOR runs in standalone mode by default. +- ``default_dark_mode``: Whether VISOR uses dark mode by default. +- ``default_log_dir``: Default directory for VISOR logs. +- ``trame_log_dir``: Directory for Trame logs. The default is ``None``, which disables Trame logging. +- ``binding_host``: Optional externally routed binding host. VISOR reads ``GLOW_PRODUCT_BINDING_HOST`` + at startup when present. Otherwise, VISOR leaves this value as ``null`` to allow per-instance host fallback. + + +Minimal example +=============== + +Here is a minimal example of a ``.visor`` file: + +.. code-block:: yaml + + app_name: "VISOR Viewer Name Override" + default_host: "localhost" + default_port: 8088 + default_standalone: False + default_dark_mode: True + default_log_dir: "/path/to/custom_log_directory" + trame_log_dir: "/path/to/trame/logs" + binding_host: null + + +Environment variables +===================== + +VISOR supports a small set of environment variables that affect runtime networking and TLS behavior: + +- ``GLOW_PRODUCT_BINDING_HOST``: When you set this variable, VISOR reads it at startup and stores the value in + ``binding_host``. Use it to provide an externally routed binding host so services and reverse proxies can + determine the externally visible hostname used for routes. + +- ``GLOW_CERTS_DIR`` and ``ANSYS_GRPC_CERTIFICATES``: VISOR checks these variables in that order. Use either + variable to point to a directory that contains platform TLS certificate files. If a matching certificate/key pair + exists (preferred filenames ``server.crt``/``server.key``, fallback filenames ``client.crt``/``client.key``), + VISOR tries to auto-configure the internal Trame/wslink server so websocket endpoints can use WSS. If + auto-configuration fails, VISOR logs the error and still starts the server. diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst new file mode 100644 index 00000000..85e9c3e1 --- /dev/null +++ b/doc/source/getting_started/index.rst @@ -0,0 +1,73 @@ +.. _getting-started-index: + +Getting started +############### + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-outlined:`build;2em` + :link-type: doc + :link: /getting_started/prerequisites + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Prerequisites</span> + + Check that you have the knowledge, software, and design prerequisites that + you need for running VISOR. + + .. grid-item-card:: :material-outlined:`file_download;2em` + :link-type: doc + :link: /getting_started/installation + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Installation</span> + + Install the VISOR package. + + .. grid-item-card:: :material-outlined:`rocket_launch;2em` + :link-type: doc + :link: /getting_started/quick_start + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Quick start</span> + + Use the Python API to quickly launch VISOR and load a VTM or VTK input file. + + .. grid-item-card:: :material-outlined:`warning;2em` + :link-type: doc + :link: /getting_started/known_limitations + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Known limitations</span> + + See known limitations of the VISOR release. + + + .. grid-item-card:: :material-outlined:`settings;2em` + :link-type: doc + :link: /getting_started/configure_visor + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">VISOR configuration</span> + + Override default VISOR settings using a project-level YAML file. + + +.. toctree:: + :hidden: + :maxdepth: 2 + + prerequisites + installation + quick_start + known_limitations + configure_visor + + + diff --git a/doc/source/getting_started/installation.rst b/doc/source/getting_started/installation.rst new file mode 100644 index 00000000..124d554d --- /dev/null +++ b/doc/source/getting_started/installation.rst @@ -0,0 +1,147 @@ +.. _installation-index: + +Installation +############# + +.. role:: ansys-gold + +User installation +----------------- + +You can run VISOR with Python 3.11 through Python 3.14 on Windows, macOS, and Linux. + +Create and activate a virtual environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To avoid conflicts with other Python packages, create and activate a `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ +before you install VISOR. + +#. Create a virtual environment: + + .. code:: console + + python -m venv .venv + +#. Activate the virtual environment for your operating system. + + On Windows, run this command: + + .. code:: console + + .venv\Scripts\activate + + + On Linux and macOS, run this command: + + .. code:: console + + source .venv/bin/activate + +Install VISOR +~~~~~~~~~~~~~ + +In the virtual environment, install VISOR with all optional dependencies: + +.. code:: console + + python -m pip install ansys-visor-viewer + + +Developer installation +---------------------- + +#. Check that the following prerequisites are installed: + + - `Poetry <https://python-poetry.org/docs/>`_ + - `Node.js & npm <https://nodejs.org/en/download/>`_ + +#. Clone the VISOR repository: + + .. code:: console + + git clone https://github.com/ansys/visor.git + cd visor + +#. Create a virtual environment: + + .. code:: console + + python -m venv .venv + +#. Activate the virtual environment for your operating system. + + On Windows, run this command: + + .. code:: console + + .venv\Scripts\activate + + On Linux and macOS, run this command: + + .. code:: console + + source .venv/bin/activate + +#. Install and build VISOR in editable mode within the virtual environment: + + .. code:: console + + # Install VISOR Python dependencies and the setup script + poetry install --with setup + + # Run the setup script to copy WASM modules to the correct location + visor-setup + + # Build the frontend + cd src\ansys\visor\visor-client + npm install + npm run build + cd ..\..\..\.. # Go back to the root of the VISOR repository + + # Install Dash component + cd src\ansys\visor\dash + npm install + npm run build + cd ..\..\..\.. # Go back to the root of the VISOR repository + + +#. Verify your installation by importing the module: + +.. code:: pycon + + >>> from ansys.visor.viewer import Visor + + +#. Clean the build environment (optional) + +If you want to create a production-like installation on Windows, first install Chocolatey as described in +[Installing Chocolatey](https://chocolatey.org/install). You do not need this step on other operating systems. + +Then run these commands: + + +.. code-block:: + + choco install make # install GNU make on Windows + make clean # clean + + +If you get a `find` command error on Windows, your shell might be calling the Windows version of `find` instead +of the expected Unix command. + +1. Verify that Git Bash is installed, for example in `C:\Program Files\Git\usr\bin`. + +2. Add `C:\Program Files\Git\usr\bin` to your system `PATH` before the `System32` path. + +For example: + +.. image:: ../../developer_docs/images/env_vars.png + :alt: Environment variables + :align: center + +.. image:: ../../developer_docs/images/system_env_vars.png + :alt: System environment variables + :align: center + + + diff --git a/doc/source/getting_started/known_limitations.rst b/doc/source/getting_started/known_limitations.rst new file mode 100644 index 00000000..f96c1e7a --- /dev/null +++ b/doc/source/getting_started/known_limitations.rst @@ -0,0 +1,17 @@ +.. _known-limitations: + +Known limitations +################# + +In this release, you might encounter the following known limitations: + +- You cannot use the cross-section widget's ``cut mesh`` button. +- When you select mesh parts colored by a variable, the selection color does not blend with the variable color. +- When you hide a mesh part, its parent appears as hidden in the tree view, even if other child parts are still visible. +- When you update the VTK dataset (the currently loaded file) with ``/update``, the frontend UI does not reset automatically. + Reload the page to resync the UI with the current geometry state. +- When you right-click objects in the render window, the part details popup does not appear. +- When you hide parts, the bounding box outline does not resize automatically. + +The first two limitations come from the underlying VTK library (version 9.5.0) and should be addressed in +future VTK releases. The remaining limitations are expected to be addressed in future VISOR releases. diff --git a/doc/source/getting_started/prerequisites.rst b/doc/source/getting_started/prerequisites.rst new file mode 100644 index 00000000..3b58dace --- /dev/null +++ b/doc/source/getting_started/prerequisites.rst @@ -0,0 +1,27 @@ +.. _prerequisites-index: + +Prerequisites +############# + +.. role:: ansys-gold + +Check that you have the knowledge, software, and design prerequisites that +you need for running VISOR. + +Python interpreter +****************** + +.. note:: + :class: note + + These Python versions are supported: :bdg-success:`3.11` :bdg-success:`3.12` :bdg-success:`3.13` :bdg-success:`3.14` + + +To use VISOR, you must have a supported Python package installed on +your system. You can download the latest version of Python from the official +`Python website <https://www.python.org/downloads/>`_, or follow the +`Python installation instructions <https://dev-docs.solutions.ansys.com/version/stable/getting_started/prerequisites/python_interpreter.html>`_ +in the Ansys Solution Application Framework documentation. + + + diff --git a/doc/source/getting_started/quick_start.rst b/doc/source/getting_started/quick_start.rst new file mode 100644 index 00000000..5bd4e635 --- /dev/null +++ b/doc/source/getting_started/quick_start.rst @@ -0,0 +1,27 @@ +.. _quick-start: + +Quick start +########### + +Use the VISOR Python API to quickly create and run a minimal SAF-based solution. + +#. Run the following code to create a VISOR instance and load a VTM file: + + .. code-block:: python + + from ansys.visor.viewer import Visor + + an_input_file = "your_file.vtm" + visualizer = Visor( + url="http://localhost:8888" + ) # optional; default is "http://localhost:8081" + visualizer.start(input=an_input_file) + +#. Open the VISOR standalone app at the specified URL. + This example uses http://localhost:8888. + +The following image shows a loaded VTM file. VISOR supports both VTM and VTK files. + +.. image:: /_static/visor_standalone_many_blocks_localhost_8081_2025-07030.png + :alt: VISOR standalone app with many blocks in the loaded file + :align: center diff --git a/doc/source/http_api_reference/index.rst b/doc/source/http_api_reference/index.rst new file mode 100644 index 00000000..142fe593 --- /dev/null +++ b/doc/source/http_api_reference/index.rst @@ -0,0 +1,13 @@ +****************** +HTTP API reference +****************** + +This section provides documentation for the VISOR HTTP API service, including available endpoints, +usage examples, and expected responses. + +Use these endpoints to interact programmatically with VISOR. + +.. openapi:: openapi.json + :paths: /start /update /stop /initialize /info /health + :examples: + diff --git a/doc/source/includes/warnings/visor_jupyter_note.rst b/doc/source/includes/warnings/visor_jupyter_note.rst new file mode 100644 index 00000000..dfc081ea --- /dev/null +++ b/doc/source/includes/warnings/visor_jupyter_note.rst @@ -0,0 +1,4 @@ +.. note:: + + VISOR does not support use within Jupyter notebooks or iPython environments. + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 00000000..fca7433d --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,104 @@ +VISOR +===== + +.. toctree:: + :hidden: + :maxdepth: 5 + + getting_started/index + user_guide/index + python_api_reference + http_api_reference/index + examples/index + contributing/index + changelog + + +Introduction +------------ + +VISOR (Visual Interactive Simulation Object Renderer) +is a Python-based 3D visualization web component for Ansys solutions and apps. + +You can use VISOR for 3D visualization on desktop and on premises. It customizes existing +rendering frameworks to meet functional and nonfunctional requirements, including integrability, +scalability, performance, and ease of use. + +You can use VISOR to integrate with the following tools: + +* `SAF (Solution Application Framework) <https://upgraded-carnival-wn6lkym.pages.github.io>`_ + to provide 3D visualization capabilities. +* `Ansys Dynamic Reporting <https://nexusdemo.ensight.com/docs/html/Nexus.html>`_ + to support dynamic reporting workflows. +* `PyAnsys Visualization Interface Tool <https://visualization-interface.tools.docs.pyansys.com/>`_ + to connect PyAnsys libraries to different plotting backends. + +You run VISOR with VTK (Visual Toolkit) rendering through Trame for client-server architecture and Python +integration workflows. You target VTK WASM with WebGL support. + +View a `demo <https://theia.dev.ansysapis.com/index.html>`_ to see VISOR in action. + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-outlined:`play_circle;2em` + :link-type: doc + :link: getting_started/index + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Getting started</span> + + Learn how to install VISOR, set up and run a minimal solution, review known + limitations, and override default settings. + + .. grid-item-card:: :material-outlined:`menu_book;2em` + :link-type: doc + :link: user_guide/index + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">User guide</span> + + Learn how to run VISOR in different modes, use its arguments, bring in your data, + and integrate with SAF. + + .. grid-item-card:: :material-outlined:`api;2em` + :link-type: doc + :link: python_api_reference + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">API reference</span> + + Access the Python API reference documentation for VISOR, including classes and methods. + + .. grid-item-card:: :material-outlined:`api;2em` + :link-type: doc + :link: http_api_reference/index + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">HTTP API reference</span> + + Access the HTTP API reference documentation for VISOR services, including endpoint + usage and example responses. + + .. grid-item-card:: :material-outlined:`lightbulb;2em` + :link-type: doc + :link: examples/index + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Examples</span> + + Explore examples demonstrating VISOR usage. + + + + + +Project index +------------- + +* :ref:`genindex` \ No newline at end of file diff --git a/doc/source/python_api_reference.rst b/doc/source/python_api_reference.rst new file mode 100644 index 00000000..3d7221a0 --- /dev/null +++ b/doc/source/python_api_reference.rst @@ -0,0 +1,15 @@ +.. _classdocumentation: + +************* +API reference +************* + +This section provides documentation for the VISOR Python API, including available classes and methods. +For information on how to use the VISOR Python API, see the :ref:`user-guide-index`. + +.. autosummary:: + :toctree: _autosummary/ + + ansys.visor.viewer.Visor + +.. include:: /includes/warnings/visor_jupyter_note.rst diff --git a/doc/source/user_guide/convert_data/grid.rst b/doc/source/user_guide/convert_data/grid.rst new file mode 100644 index 00000000..acc485b2 --- /dev/null +++ b/doc/source/user_guide/convert_data/grid.rst @@ -0,0 +1,11 @@ + +Use VISOR as a 3D visualization tool for solution apps. VISOR uses VTK as its internal format. +You can work with in-memory data and files in these formats: + +- ``vtkPolyData`` +- ``vtkUnstructuredGrid`` +- ``vtkMultiBlockDataSet`` +- ``vtkPartitionedDataSet`` + +If your source data uses another format, use a conversion tool to convert it to a VTK format. + diff --git a/doc/source/user_guide/convert_data/index.rst b/doc/source/user_guide/convert_data/index.rst new file mode 100644 index 00000000..f41434ad --- /dev/null +++ b/doc/source/user_guide/convert_data/index.rst @@ -0,0 +1,13 @@ +.. _visor-data: + + +Bring data into VISOR +##################### + +.. include:: grid.rst + + +.. toctree:: + :maxdepth: 2 + :hidden: + diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst new file mode 100644 index 00000000..61ae27cc --- /dev/null +++ b/doc/source/user_guide/index.rst @@ -0,0 +1,51 @@ +.. _user-guide-index: + +User guide +########## + + +Launch VISOR +============ + +.. include:: /user_guide/launching_visor/grid.rst + + +Modify a VISOR scene +===================== + +.. include:: /user_guide/update_scene/grid.rst + + +Bring data into VISOR +===================== + +.. include:: /user_guide/convert_data/grid.rst + + +Use VISOR in a SAF solution +=========================== + +.. include:: /user_guide/saf_integration/grid.rst + + +Understand the VISOR UI +======================= + +.. include:: /user_guide/visualizer_ui/grid.rst + + + +.. toctree:: + :maxdepth: 2 + :hidden: + + + launching_visor/index + update_scene/index + convert_data/index + saf_integration/index + visualizer_ui/index + + + + diff --git a/doc/source/user_guide/launching_visor/grid.rst b/doc/source/user_guide/launching_visor/grid.rst new file mode 100644 index 00000000..ddb510a2 --- /dev/null +++ b/doc/source/user_guide/launching_visor/grid.rst @@ -0,0 +1,58 @@ + +To run VISOR, you provide a VTK or VTM input file. If you want, you can include +optional metadata in start and update requests. VISOR's three modes of operation let +you pick the launch method that best matches your workflow. + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-outlined:`input;2em` + :link-type: doc + :link: /user_guide/launching_visor/input + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Supply an input file</span> + + Learn which input formats VISOR supports. + + .. grid-item-card:: :material-outlined:`description;2em` + :link-type: doc + :link: /user_guide/launching_visor/metadata + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Use metadata</span> + + Learn how to use metadata in start and update requests. + + .. grid-item-card:: :material-outlined:`add_circle;2em` + :link-type: doc + :link: /user_guide/launching_visor/visor_python_api + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Launch VISOR using the Python API</span> + + Launch VISOR from a Python script using the Python API. + + .. grid-item-card:: :material-outlined:`download_done;2em` + :link-type: doc + :link: /user_guide/launching_visor/visor_service + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Launch VISOR using the HTTP API service</span> + + Launch VISOR with the HTTP API service and manage multiple 3D visualization instances. + + .. grid-item-card:: :material-outlined:`dashboard_customize;2em` + :link-type: doc + :link: /user_guide/launching_visor/visor_dash_component + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Use the VISOR Dash component</span> + + Run VISOR and connect to it in a Dash app with the VISOR Dash component. + diff --git a/doc/source/user_guide/launching_visor/index.rst b/doc/source/user_guide/launching_visor/index.rst new file mode 100644 index 00000000..2f1334df --- /dev/null +++ b/doc/source/user_guide/launching_visor/index.rst @@ -0,0 +1,19 @@ +.. _run-index: + +Launch VISOR +############ + + +.. include:: grid.rst + + + +.. toctree:: + :maxdepth: 2 + :hidden: + + input + metadata + visor_python_api + visor_service + visor_dash_component diff --git a/doc/source/user_guide/launching_visor/input.rst b/doc/source/user_guide/launching_visor/input.rst new file mode 100644 index 00000000..21c0f946 --- /dev/null +++ b/doc/source/user_guide/launching_visor/input.rst @@ -0,0 +1,64 @@ +.. _visor-input-format: + +Supply an input file +#################### + +You can use VTK and VTM files as input. +VISOR reads the following VTK data types: + +* ``vtkMultiBlockDataSet`` +* ``vtkMultiPieceDataSet`` +* ``vtkUnstructuredGrid`` +* ``vtkPolyData`` + +Supported inputs for the VISOR HTTP service +########################################### + +When you use the VISOR HTTP service, the ``/start`` and ``/update`` endpoints accept a ``file_path`` parameter +that specifies the input file path. The file path must be accessible from the machine where the VISOR +service is running. + +You can also use the ``visor-cli`` tool, which calls the VISOR HTTP service. The ``/start`` and ``/update`` +endpoints accept a ``--file-path`` argument to specify the input file. + +The HTTP API does not support streaming file content directly in the request body. + +The following examples show a ``/start`` payload and the equivalent VISOR CLI command: + +.. tab-set:: + + .. tab-item:: Start payload + + .. code-block:: json + + { + "file_path": "path/to/your_file.vtm", + "metadata": { + "name": "My Visualization", + "unit": "cm" + }, + "timeout": 60 + } + + .. tab-item:: VISOR CLI command + + .. code-block:: bash + + visor-cli start --file-path path/to/your_file.vtm --name "My Visualization" --unit "cm" + + +Supported inputs for the VISOR Python API +######################################### + +You can use the VISOR Python API with input files and in-memory VTK objects. + +To provide an input file path, pass a string to the ``input`` parameter of the ``Visor`` class: + +.. code-block:: python + + from ansys.visor.viewer import Visor + + an_input_file = "path/to/your_file.vtm" + + visualizer = Visor() + visualizer.start(input=an_input_file) diff --git a/doc/source/user_guide/launching_visor/metadata.rst b/doc/source/user_guide/launching_visor/metadata.rst new file mode 100644 index 00000000..143db494 --- /dev/null +++ b/doc/source/user_guide/launching_visor/metadata.rst @@ -0,0 +1,157 @@ +.. _visor-metadata: + +Use metadata +############ + +VISOR supports an optional ``metadata`` argument in start and update requests. +Providing metadata is recommended for improved visualization context. + +The ``metadata`` argument can include the following fields: + +- ``name``: String representing the name of the dataset or visualization. +- ``unit``: String representing the unit of measurement for the dataset (for example, ``"cm"``, ``"m"``, + or ``"inches"``). +- ``state``: Optional dictionary that can include initial per-part values. Only the initial opacity + is supported as a per-part property. The structure of the state dictionary is as follows: + This information is useful for understanding the scale of the data and is displayed in the VISOR user interface. +- ``state``: Optional dictionary that can include initial per-part values. Only the initial opacity + is supported as a per-part property. The structure of the state dictionary is as follows: + + - ``parts``: Dictionary where keys are part names (strings) and values are dictionaries with properties. + - ``opacity``: Float between 0.0 (fully transparent) and 1.0 (fully opaque) representing the initial opacity + of the part. + +VISOR HTTP API +~~~~~~~~~~~~~~ + +When using the VISOR HTTP service, the ``/start`` and ``/update`` endpoints accept an optional ``metadata`` parameter +in the request payload. + +.. important:: + + The ``metadata`` argument should be a dictionary matching the structure of the ``Metadata`` class. + +Minimal example: + +.. tab-set:: + + .. tab-item:: Start pyload + + .. code-block:: json + + { + "file_path": "path/to/your_file.vtm", + "metadata": { + "name": "My Visualization", + "unit": "cm" + }, + "timeout": 60 + } + + .. tab-item:: VISOR CLI command + + .. code-block:: bash + + # Using visor-cli to start VISOR with metadata + # metadata.json has content {"name": "My Visualization", "unit": "cm"} + + visor-cli start --file-path path/to/your_file.vtm --metadata-path metadata.json + +Example including per-part opacities: + +.. tab-set:: + + .. tab-item:: Start payload + + .. code-block:: json + + { + "file_path": "path/to/your_file.vtm", + "metadata": { + "name": "My Visualization", + "unit": "cm", + "state": { + "parts": { + "part1": {"opacity": 0.25}, + "part2": {"opacity": 0.75} + } + } + }, + "timeout": 60 + } + + .. tab-item:: VISOR CLI command + + .. code-block:: bash + + # Metadata can be passed in a JSON file, for example: + # { + # "name": "My Visualization", + # "unit": "cm", + # "state": { + # "parts": { + # "part1": {"opacity": 0.25}, + # "part2": {"opacity": 0.75} + # } + # } + # } + + visor-cli start --file-path path/to/your_file.vtm --metadata-path metadata.json + + +VISOR Python API +~~~~~~~~~~~~~~~~ + +When using the Python API, the ``Visor`` class accepts an optional ``metadata`` parameter in its constructor. + +The ``metadata`` argument needs to be an instance of the ``Metadata`` class. + +Minimal example: + +.. code-block:: python + + from ansys.visor.viewer import Visor + from ansys.visor.viewer import Metadata + + an_input_file = "path/to/your_file.vtm" + metadata = Metadata(name="My Visualization", unit="cm") + + visualizer = Visor() + visualizer.start(input=an_input_file, metadata=metadata) + +Additional Example: + +The ``Metadata`` class also supports an optional ``state`` field to define initial per-part values. +Only the initial opacity is supported as a per-part property. + +.. code-block:: python + + from ansys.visor.viewer import Visor + from ansys.visor.viewer import Metadata + + # Path to file. Assume the file contains parts named "part1" and "part2". + an_input_file = "path/to/your_file.vtm" + + # Create metadata with name, unit, and initial opacity state for parts. + metadata = Metadata( + name="My Visualization", + unit="cm", + state={ + "parts": { + "part1": {"opacity": 0.25}, + "part2": {"opacity": 0.75}, + }, + }, + ) + visualizer = Visor() + visualizer.start(input=an_input_file, metadata=metadata) + +Reference +~~~~~~~~~ + +Reference: ``Metadata`` class (`source code <https://github.com/ansys-internal/theia/blob/main/src/ansys/theia/viewer/core/metadata.py>`_). + +.. literalinclude:: ../../../../src/ansys/visor/viewer/core/metadata.py + :pyobject: Metadata + :language: python + diff --git a/doc/source/user_guide/launching_visor/visor_dash_component.rst b/doc/source/user_guide/launching_visor/visor_dash_component.rst new file mode 100644 index 00000000..eeab6005 --- /dev/null +++ b/doc/source/user_guide/launching_visor/visor_dash_component.rst @@ -0,0 +1,91 @@ +.. _visor-dash-component: + + +Use the VISOR Dash component +############################ + +Use VISOR in a Dash app with the ``Visordash`` component. + +The Dash component creates a WebSocket connection to a running VISOR instance on a given port. +The following example shows how to connect the Dash component to a VISOR instance. It uses +the following ports: + +- VISOR app server port: ``8081`` +- VISOR Dash component port: ``8050`` + +With these ports, the VISOR Dash server is available at ``http://localhost:8050``. +The VISOR Dash server connects to the VISOR app server at ``http://localhost:8081``. + +Start the VISOR app server first with the VISOR CLI or Python API. + +**Example** + +To use the VISOR Dash component, start a VISOR instance and then run the Dash app +that connects to that instance. + +This example uses a VISOR instance running on ``http://localhost:8081``. + +#. Start the VISOR HTTP server: + + .. code-block:: bash + + visor-cli server start + +#. Start a VISOR instance with a VTM file using either the VISOR CLI or Python API: + + .. tab-set:: + + .. tab-item:: VISOR CLI + + .. code-block:: bash + + visor-cli instance start your_file.vtm + + .. tab-item:: Python API + + .. code-block:: python + + from ansys.visor.viewer import Visor + + visualizer = Visor( + url="http://localhost:8081", + input="your_file.vtm", + ) + visualizer.start() + + +#. Run the Dash app: + + .. code-block:: python + + from visordash import init_endpoints, Visordash + from dash import Dash, html + + app = Dash(__name__) + + VISOR_PORT = 8081 # Port where VISOR instance is running + DASH_PORT = 8050 # Port where Dash app runs + + init_endpoints(app) + + td = Visordash( + id="input", + value="my-value", + label="my-label", + host="localhost", + port=VISOR_PORT, + ) + + app.layout = html.Div([td, html.Div(id="output")]) + + app.run( + host="0.0.0.0", port=DASH_PORT, debug=False, use_reloader=False + ) # use_reloader=False avoids double execution + + +.. note:: + + Start the VISOR app server in the same Dash app or through the VISOR HTTP API. + Use the default URL (``http://localhost:8081``), or set a custom URL in both the Dash component and + the VISOR app server. + diff --git a/doc/source/user_guide/launching_visor/visor_python_api.rst b/doc/source/user_guide/launching_visor/visor_python_api.rst new file mode 100644 index 00000000..18be927f --- /dev/null +++ b/doc/source/user_guide/launching_visor/visor_python_api.rst @@ -0,0 +1,154 @@ +.. _visor-python-api: + +Launch VISOR using the Python API +################################# + +In a Python app, use the ``Visor`` class from the ``ansys.visor.viewer`` package to visualize a file or a VTK dataset object. +This page includes simple examples that show how to visualize VTK and VTM files and in-memory VTK dataset objects. + +For more comprehensive examples, see the :ref:`gallery` and :ref:`classdocumentation` sections. + +Load a VTK or VTM file +********************** + +VISOR supports VTK and VTM files. For information on supported VTK data types, see +:ref:`visor-input-format`. + +The following code starts a desktop VISOR app, opens an input file, and starts a standalone app on +``http://localhost:8081``. + +.. code-block:: python + + from ansys.visor.viewer import Visor + + an_input_file = "your_file.vtm" + visualizer = Visor() + # Alternatively, set the url parameter to change the default URL, for example: + # visualizer = Visor(url="http://localhost:8888") + visualizer.start(input=an_input_file) + +To use a different URL, set the ``url`` parameter when you initialize the ``Visor`` class. + +This image shows an example with a VTM file: + +.. image:: /_static/visor_standalone_many_blocks_localhost_8081_2025-07030.png + :alt: VISOR Standalone Many Blocks Example + :align: center + +Load a VTK dataset object +************************* + +You can visualize in-memory VTK objects. The following code uses +`vtkCubeSource <https://vtk.org/doc/nightly/html/classvtkCubeSource.html>`_ from VTK to visualize a simple cube. +It starts a desktop VISOR app, opens the cube object, and starts a standalone app on ``http://localhost:8081``. + +.. code-block:: python + + from vtk import vtkCubeSource + from ansys.visor.viewer import Visor + + + # Create a cube source and get the output as a vtkPolyData object + def get_cube(x: float, y: float, z: float): + cube_source = vtkCubeSource() + cube_source.SetXLength(x) + cube_source.SetYLength(y) + cube_source.SetZLength(z) + cube_source.Update() + return cube_source.GetOutput() + + + cube = get_cube(1.0, 2.0, 3.0) + + # Visualize the cube using VISOR + visualizer = Visor() + visualizer.start(input=cube) + +Update the VISOR visualization +****************************** + +After the VISOR server starts, call the ``update()`` method to update the visualization. + +.. code-block:: python + + # Update the visualization to display a new input file. + visualizer.update("new_input_file.vtm") + + +You can also pass a VTK dataset object to the ``update()`` method. +The following code updates the previously created cube to a sphere: + +.. code-block:: python + + from vtk import vtkSphereSource + + + # Create a sphere source and get the output as a vtkPolyData object + def get_sphere(radius: float, theta_res: int, phi_res: int): + sphere_source = vtkSphereSource() + sphere_source.SetRadius(radius) + sphere_source.SetThetaResolution(theta_res) + sphere_source.SetPhiResolution(phi_res) + sphere_source.Update() + return sphere_source.GetOutput() + + + sphere = get_sphere(1.0, 32, 32) + + # Update the visualization to show the sphere instead of the cube + visualizer.update(sphere) + + +Get information about the VISOR instance +**************************************** + +Use the following ``Visor`` properties to get information about the running instance: + +.. code-block:: python + + # Get the URL where the VISOR server is running + url = visualizer.url + print(f"VISOR server is running at: {url}") + + # Get the host where the VISOR server is running + host = visualizer.host + print(f"VISOR server is running on host: {host}") + + # Get the port where the VISOR server is running + port = visualizer.port + print(f"VISOR server is running on port: {port}") + + # Get a dictionary of info about the VISOR instance + info = visualizer.info + # e.g. + # {'app_name': 'VISOR Viewer', + # 'host': 'localhost', + # 'port': 8081, + # 'standalone': True, + # 'file_input_path': '../../examples/assets/tensors9.vtp', + # 'metadata': None} + + +Stop the VISOR visualization +**************************** + +Call ``stop()`` to stop the VISOR server: + +.. code-block:: python + + # Stop the VISOR server + visualizer.stop() + print("VISOR server has been stopped.") + +Rendering engine +**************** + +VISOR uses VTK.wasm as its default (and only supported) rendering engine. +VISOR uses Trame to support VTK.wasm. + + +Further references +****************** + +* For comprehensive Python API documentation, see :ref:`classdocumentation`. +* For usage examples, see :ref:`gallery`. diff --git a/doc/source/user_guide/launching_visor/visor_service.rst b/doc/source/user_guide/launching_visor/visor_service.rst new file mode 100644 index 00000000..bd289c0a --- /dev/null +++ b/doc/source/user_guide/launching_visor/visor_service.rst @@ -0,0 +1,349 @@ +.. _visor-service: + +Launch VISOR using the HTTP API service +####################################### + +Use the VISOR HTTP API service to manage multiple 3D visualization instances. +Each instance uses a host and port that you specify, and each instance runs on a dedicated Trame server. + +The service endpoints let you perform the following tasks: + +* Initialize and shut down VISOR instances. +* Start and stop visualizations. +* Update input files and metadata. +* Switch between multiple instances. +* Query the state of the service. + +This setup lets you manage multiple, independently configured visualization instances. +You can switch between datasets or configurations, start or stop visualizations on demand, +and update assets without restarting the full service. + +Use the VISOR service +~~~~~~~~~~~~~~~~~~~~~ + +To interact with the VISOR service APIs, choose one of the following options: + +- Run the APIs directly using HTTP requests. +- Use the ``visor-cli`` tool, which provides a command-line interface for the service. + +#. Run the VISOR APIs with HTTP endpoints. + + The VISOR service exposes RESTful HTTP endpoints that you can access with any HTTP client library. + You can start with one of the following options: + + * OpenAPI documents page at ``http://localhost:53211/docs``. + + * Use this interactive page to test API endpoints. + * View available endpoints, parameters, and responses. + * Send requests directly from your browser without writing code. + + * Python ``requests`` library or another HTTP client library. + + Subsequent examples show payloads for each endpoint. + You can send requests with any HTTP client. + + +#. Use the VISOR CLI tool. + + The ``visor-cli`` tool provides a command-line interface for the VISOR HTTP service. + It simplifies starting, updating, and stopping VISOR instances without manually crafting HTTP requests. + +Subsequent examples show both Python ``requests`` and the ``visor-cli`` tool. + + +Start the VISOR service +~~~~~~~~~~~~~~~~~~~~~~~ + +To start the VISOR HTTP service, choose one of the following options. + +**Examples** + +Use the tabs to switch between seeing the ``uvicorn`` command and the VISOR CLI. + +.. tab-set:: + + .. tab-item:: Uvicorn command + + .. code-block:: bash + + uvicorn ansys.visor.viewer.api.server:app --host localhost --port 53211 + + .. tab-item:: VISOR CLI + + .. code-block:: bash + + # start the VISOR HTTP service on the default host (localhost) and port (53211) + visor-cli server start + + # OR, to start the VISOR HTTP service on a custom host and port, run + visor-cli --api-host localhost --api-port 53211 server start + + +This command starts the ``uvicorn`` service and initializes the VISOR server, but it does not +start a VISOR instance. The service listens on port ``53211``. You can access API endpoints +at ``http://localhost:53211``. + +After the service starts, use the API to start, update, and stop VISOR instances. + + +Start the API +^^^^^^^^^^^^^ + +Use the ``/start`` endpoint to send a POST request with a file path and metadata. + +Start endpoint: ``http://localhost:53211/start`` + +Start payload: + +.. code-block:: json + + { + "file_path": "<full repo path>/visor/tests/files/many_blocks/many_blocks.vtm", + "metadata": { + "name": "Many Blocks Asset", + "unit": "cm" + }, + "timeout": 60 + } + +Response: + +.. code-block:: json + + { + "success": "Server started on http://localhost:8081" + } + +The app starts at ``http://localhost:8081/index.html``. + +**Examples** + +Use the tabs to switch between seeing the Python and VISOR CLI examples. + +.. tab-set:: + + .. tab-item:: Python (requests) + + .. code-block:: python + + import requests + + payload = { + "file_path": "tests/files/many_blocks/many_blocks.vtm", + "metadata": {"name": "Many Blocks Asset", "unit": "cm"}, + "timeout": 60, + } + response = requests.post("http://localhost:53211/start", json=payload) + print(response.json()) + {"success": "Server started on http://localhost:8081"} + + .. tab-item:: VISOR CLI + + .. code-block:: bash + + # Start a VISOR instance with the specified file and metadata + # Metadata file contains {"name": "Many Blocks Asset", "unit": "cm"} + + visor-cli instance start tests/files/many_blocks/many_blocks.vtm --metadata-path tests/files/many_blocks/metadata.json --timeout 60 + {'success': 'Server started on http://localhost:8081'} + + +Update the API +^^^^^^^^^^^^^^ + +The ``/update`` endpoint updates the running VISOR instance with a new file. + +Update endpoint: ``http://localhost:53211/update`` + +Update payload: + +.. code-block:: json + + { + "file_path": "visor/tests/files/plate.vtp", + "metadata": { + "name": "Plate Asset", + "unit": "cm" + } + } + +Response: + +.. code-block:: json + + { + "success": "Server input updated on http://localhost:8081" + } + +The viewer replaces the old asset with the new ``file_path`` value. + +**Examples** + +Use the tabs to switch between Python and the VISOR CLI examples. + +.. tab-set:: + + .. tab-item:: Python (requests) + + .. code-block:: python + + import requests + + payload = { + "file_path": "visor/tests/files/plate.vtp", + "metadata": {"name": "Plate Asset", "unit": "cm"}, + "timeout": 60, + } + response = requests.post("http://localhost:53211/update", json=payload) + print(response.json()) + {"success": "Server updated on http://localhost:8081"} + + .. tab-item:: VISOR CLI + + .. code-block:: bash + + # Start a VISOR instance with the specified file and metadata + # plate_metadata.json file contains {"name": "Plate Asset", "unit": "cm"} + + visor-cli instance update visor/tests/files/plate.vtp --metadata-path visor/tests/files/plate_metadata.json + {'success': 'Server updated on http://localhost:8081'} + + +Stop the visualization API +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``/stop_visualization`` endpoint stops the active VISOR instance. + +Stop visualization endpoint: ``http://localhost:53211/stop_visualization`` + +Response: + +.. code-block:: json + + { + "success": "Stopped visualization for server running on http://localhost:8081" + } + +**Examples** + +Use the tabs to switch between seeing the Python and VISOR CLI examples. + +.. tab-set:: + + .. tab-item:: Python (requests) + + .. code-block:: python + + import requests + + response = requests.post("http://localhost:53211/stop_visualization") + print(response.json()) + {"success": "Stopped visualization for server running on http://localhost:8081"} + + .. tab-item:: VISOR CLI + + .. code-block:: bash + + visor-cli instance stop_visualization + {'success': 'Stopped visualization for server running on http://localhost:8081'} + + + +Stop the API +^^^^^^^^^^^^ + +The ``/stop`` endpoint stops the active VISOR server and deletes the instance. + +Stop endpoint: ``http://localhost:53211/stop`` + +Response: + +.. code-block:: json + + { + "success": "Stopped visualization for server running on http://localhost:8081 and deleted instance" + } + + +**Examples** + +Use the tabs to switch between seeing the Python and VISOR CLI examples. + +.. tab-set:: + + .. tab-item:: Python (requests) + + .. code-block:: python + + import requests + + response = requests.post("http://localhost:53211/stop") + print(response.json()) + { + "success": "Stopped visualization for server running on http://localhost:8081 and deleted instance" + } + + .. tab-item:: VISOR CLI + + .. code-block:: bash + + visor-cli instance stop + {'success': 'Stopped visualization for server running on http://localhost:8081 and deleted instance'} + + +Initialize the API +^^^^^^^^^^^^^^^^^^ + +The ``/initialize`` endpoint creates a new VISOR instance at the provided host and port. +If ``port`` is set to ``0``, the server selects an unused port on the host. +The service creates the instance but does not start it. If an instance already exists at the +specified host and port, the service retrieves it and sets it as the active instance. + +Initialize endpoint: ``http://localhost:53211/initialize`` + +Initialize payload: + +.. code-block:: json + + { + "host": "localhost", + "port": 8082, + "standalone": true + } + +Response: + +.. code-block:: json + + { + ["Set active instance to http://localhost:8082"] + } + + +**Examples** + +Use the tabs to switch between seeing the Python and VISOR CLI examples. + +.. tab-set:: + + .. tab-item:: Python (requests) + + .. code-block:: python + + import requests + + payload = {"host": "localhost", "port": 8082, "standalone": True} + response = requests.post("http://localhost:53211/initialize", json=payload) + print(response.json()) + ["Set active instance to http://localhost:8082"] + + .. tab-item:: VISOR CLI + + .. code-block:: bash + + visor-cli server init --host localhost --port 8082 + ['Set active instance to http://localhost:8082'] + + + + diff --git a/doc/source/user_guide/saf_integration/grid.rst b/doc/source/user_guide/saf_integration/grid.rst new file mode 100644 index 00000000..83639063 --- /dev/null +++ b/doc/source/user_guide/saf_integration/grid.rst @@ -0,0 +1,45 @@ + + +VISOR is a shared technology component (STC) within the Solutions Application Framework (SAF) that provides a 3D +visualization environment for simulation data. +VISOR is designed to be integrated into SAF solutions, allowing users to visualize and interact with simulation results +in a consistent and user-friendly manner. + +Product Instance Manager +------------------------ + +VISOR is exposed in the SAF solution through its Product Instance Management (PIM) functionality, +which provides a standardized interface for managing and accessing VISOR instances. +See more information in the `PIM documentation`_. + + + + +SAF integration examples +------------------------ + +Below are two examples of how to integrate VISOR into a SAF solution: + +#. **SAF VISOR POC**: + + `SAF VISOR POC`_ is a simple example that demonstrates how to integrate + VISOR into a SAF solution for interactive 3D visualization of engineering data. + +#. **SAF reference solution**: + + `Airfoil Explorer`_ is a reference solution application + showcasing integration of five key STCs through a guided workflow for defining airfoil geometry, generating a mesh, + running a 2D potential flow solve, and visualizing results. Ideal as a template for building engineering solution + apps. + + + + + + +.. _PIM documentation: + https://upgraded-carnival-wn6lkym.pages.github.io/version/stable/user_guide/backend/instance_management/index.html +.. _Airfoil Explorer: + https://github.com/ansys-internal/airfoil-explorer +.. _SAF VISOR POC: + https://github.com/ansys-internal/saf-theia-poc diff --git a/doc/source/user_guide/saf_integration/index.rst b/doc/source/user_guide/saf_integration/index.rst new file mode 100644 index 00000000..dac2b83f --- /dev/null +++ b/doc/source/user_guide/saf_integration/index.rst @@ -0,0 +1,11 @@ +.. _visor-saf-integration: + +Use VISOR in a SAF solution +=========================== + +.. include:: grid.rst + +.. toctree:: + :maxdepth: 2 + :hidden: + diff --git a/doc/source/user_guide/update_scene/add_remove_datasets.rst b/doc/source/user_guide/update_scene/add_remove_datasets.rst new file mode 100644 index 00000000..54d388da --- /dev/null +++ b/doc/source/user_guide/update_scene/add_remove_datasets.rst @@ -0,0 +1,155 @@ +.. _visor-add-remove-datasets: + +Add, list, and remove datasets +############################## + +After a VISOR instance starts, you can add or remove datasets dynamically. +You can do this through the Python API, HTTP API, or VISOR CLI. + +For more comprehensive examples, see the :ref:`gallery` and :ref:`classdocumentation` section. + +.. note:: + In the following examples, the **HTTP API** and **VISOR CLI** tabs assume that the VISOR service + is running on ``localhost:53211``. To start the service, run ``visor-cli server start``. + +Add a dataset +************* + +After a VISOR visualization starts, you can add a dataset with the ``add_dataset()`` Pythonmethod, +the HTTP API, or the VISOR CLI. + +The following examples show how to add a dataset to an existing VISOR instance. + +.. tab-set:: + + .. tab-item:: Python + + Use the ``Visor`` class in Python. + + .. code-block:: python + + # Add a new dataset + new_dataset_file = "path/to/your/new_dataset.vtm" + new_metadata_file = "path/to/your/new_dataset_metadata.json" # optional + visualizer.add_dataset(new_dataset_file, metadata_file=new_metadata_file) + + .. tab-item:: HTTP API + + Use the HTTP API with the Python ``requests`` library. + This example assumes the service is running on ``localhost:53211``. + + .. code-block:: python + + import requests + + # Add a new dataset via HTTP API + new_dataset_file = "path/to/your/new_dataset.vtm" + new_metadata_file = "path/to/your/new_dataset_metadata.json" # optional + payload = { + "file_path": new_dataset_file, + "metadata_file": new_metadata_file, # optional + } + response = requests.post(f"http://localhost:53211/add_dataset", json=payload) + + .. tab-item:: VISOR CLI + + Use the VISOR CLI in a terminal. + This example assumes the service is running on ``localhost:53211``. + + .. code-block:: bash + + # Add a new dataset via VISOR CLI + visor-cli instance add path/to/your/new_dataset.vtm + # or with metadata (optional) + visor-cli instance add path/to/your/new_dataset.vtm --metadata-path path/to/your/new_dataset_metadata.json + + +List current datasets +********************* + +Before you interact with datasets in a running VISOR instance, list the current datasets to get their IDs. +You can do this with the ``list_datasets()`` Python method, the HTTP API, or the VISOR CLI. + +.. tab-set:: + + .. tab-item:: Python + + Use the ``Visor`` class in Python. + + .. code-block:: python + + # List datasets + datasets = visualizer.list_datasets() + print("Available datasets:", datasets) + + dataset_ids = list(datasets.keys()) # Get the list of dataset IDs + dataset_id = dataset_ids[0] # Select the first dataset ID + + .. tab-item:: HTTP API + + Use the HTTP API with the Python ``requests`` library. + This example assumes the service is running on ``localhost:53211``. + + .. code-block:: python + + import requests + + # List datasets via HTTP API + response = requests.get(f"http://localhost:53211/list_datasets") + datasets = response.json() + print("Available datasets:", datasets) + + dataset_ids = list(datasets.keys()) # Get the list of dataset IDs + dataset_id = dataset_ids[0] # Select the first dataset ID + + .. tab-item:: VISOR CLI + + Use the VISOR CLI in a terminal. + This example assumes the service is running on ``localhost:53211``. + + .. code-block:: bash + + # List datasets via VISOR CLI + visor-cli instance list + # Prints a dictionary of dataset IDs and their metadata + + +Remove a dataset +**************** + +You can remove a dataset from a running VISOR instance with the ``remove_dataset()`` Python method, +the HTTP API, or the VISOR CLI. + +.. tab-set:: + + .. tab-item:: Python + + Use the ``Visor`` class in Python. + + .. code-block:: python + + # Remove a dataset + datasets = visualizer.remove_dataset(dataset_id) + + .. tab-item:: HTTP API + + Use the HTTP API with the Python ``requests`` library. + This example assumes the service is running on ``localhost:53211``. + + .. code-block:: python + + import requests + + payload = {"dataset_id": dataset_id} + # Remove datasets via HTTP API + response = requests.post(f"http://localhost:53211/remove_datasets", json=payload) + + .. tab-item:: VISOR CLI + + Use the VISOR CLI in a terminal. + This example assumes the service is running on ``localhost:53211``. + + .. code-block:: bash + + # Remove a dataset with ID <dataset_id> via VISOR CLI + visor-cli instance remove <dataset_id> diff --git a/doc/source/user_guide/update_scene/grid.rst b/doc/source/user_guide/update_scene/grid.rst new file mode 100644 index 00000000..ece2ef76 --- /dev/null +++ b/doc/source/user_guide/update_scene/grid.rst @@ -0,0 +1,27 @@ + +You can update datasets and variables values while VISOR is running. + + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-outlined:`add_circle;2em` + :link-type: doc + :link: /user_guide/update_scene/add_remove_datasets + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Add, list, and remove datasets in VISOR</span> + + Add, list, and remove datasets in an already-running VISOR instance. + + .. grid-item-card:: :material-outlined:`download_done;2em` + :link-type: doc + :link: /user_guide/update_scene/update_variables + + .. raw:: html + + <span style="font-size: 1.5em; color:#5a2a82;">Update variable values in a VISOR dataset</span> + + Update variable values in a specific dataset loaded in an already-running VISOR instance. + diff --git a/doc/source/user_guide/update_scene/index.rst b/doc/source/user_guide/update_scene/index.rst new file mode 100644 index 00000000..35a9fb47 --- /dev/null +++ b/doc/source/user_guide/update_scene/index.rst @@ -0,0 +1,13 @@ +.. _visor-update: + +Modify a VISOR scene +#################### + +.. include:: grid.rst + +.. toctree:: + :maxdepth: 2 + :hidden: + + add_remove_datasets + update_variables diff --git a/doc/source/user_guide/update_scene/update_variables.rst b/doc/source/user_guide/update_scene/update_variables.rst new file mode 100644 index 00000000..c8f5eac5 --- /dev/null +++ b/doc/source/user_guide/update_scene/update_variables.rst @@ -0,0 +1,320 @@ +.. _visor-update-variables: + +Update variable values in a VISOR dataset +######################################### + +If a dataset in VISOR contains variable arrays, you can update those values +without reloading the full dataset. Use this mechanism to push new time-step +data, simulation results, or other per-frame array changes into a live VISOR +session. + +You can use this feature through the Python API and HTTP API. +The VISOR CLI does not support this feature. + +For more comprehensive examples, see the :ref:`gallery` and :ref:`classdocumentation` sections. + +.. note:: + + In the following HTTP API examples, the VISOR service is assumed to run on + ``localhost:53211``. To start the service, run + ``visor-cli server start``. + + +Composite (multiblock) datasets +================================ + +``update_variables`` supports simple datasets (``vtkPolyData``, +``vtkUnstructuredGrid``) and composite datasets (``vtkMultiBlockDataSet``, +``vtkMultiPieceDataSet``). + +For composite datasets, each leaf block is called a **part**. Each part uses a +``part_id``, which is an opaque integer that remains stable for the current +server session but is not preserved across restarts. After you load a dataset, +call ``list_variables()`` to discover the current ``part_id`` values before +you call ``update_variables()``. + + +List variables on a dataset +**************************** + +``list_variables()`` returns a list of parts. Each part includes +``part_id``, ``part_name``, and the variables available on that part. For +non-composite datasets, the list contains exactly one entry. + +First, get the dataset ID for the dataset that you want to update: + +.. tab-set:: + + .. tab-item:: Python + + .. code-block:: python + + from ansys.visor.viewer import Visor + + visualizer = Visor(url="http://localhost:53211", input="path/to/your_file.vtm") + + datasets = visualizer.list_datasets() + dataset_id = list(datasets.keys())[0] # select the first dataset + + .. tab-item:: HTTP API + + .. code-block:: python + + import requests + + response = requests.get("http://localhost:53211/list_datasets") + datasets = response.json() + dataset_id = list(datasets.keys())[0] + +Then list variables: + +.. tab-set:: + + .. tab-item:: Python + + .. code-block:: python + + parts = visualizer.list_variables(dataset_id) + + for part in parts: + print(f"part_id={part.part_id} name={part.part_name}") + for var in part.variables: + print( + f" {var.name} type={var.type} components={var.num_components} points={var.num_points}" + ) + + The returned objects expose the following attributes: + + - ``part.part_id``: Integer ID used to target this part in ``update_variables()``. + - ``part.part_name``: Name taken from VTK block metadata. + - ``part.variables``: List of ``VisorVariable`` objects, each with + ``name``, ``type`` (``"point"`` or ``"cell"``), ``num_components``, + ``num_points``, ``ranges``, and ``magnitude_range``. + + .. tab-item:: HTTP API + + .. code-block:: python + + import requests + + response = requests.post(f"http://localhost:53211/{dataset_id}/list_variables") + result = response.json() + + # result["parts"] is a list of part objects + for part in result["parts"]: + print(f"part_id={part['part_id']} name={part['part_name']}") + for var in part["variables"]: + print(f" {var['name']} type={var['type']}") + + Response shape: + + .. code-block:: json + + { + "parts": [ + { + "part_id": 1234567890, + "part_name": "blade_1", + "variables": [ + { + "name": "temperature", + "type": "point", + "num_components": 1, + "num_points": 962 + } + ] + } + ] + } + +.. note:: + + The ``"parts"`` response shape applies to all dataset types. For + non-composite datasets, the list has exactly one entry. + + +Update variable values +********************** + +After you get ``part_id`` values from the ``list_variables()`` Python method, you +can push new data for any variable on any part. + +Each entry in the update list uses this structure: + +.. code-block:: python + + { + "type": "point", # or "cell" + "name": "variable_name", # must match an existing array name + "num_components": 1, # 1 for scalar, 3 for vector, etc. + "data": [...], # flat array, length = num_points * num_components + "part_id": 1234567890, # required for composite datasets; omit for non-composite + } + + +Target a specific part (composite datasets) +-------------------------------------------- + +Provide ``part_id`` to update a single leaf block. Get the ``part_id`` +directly from the ``list_variables()`` response. + +.. tab-set:: + + .. tab-item:: Python + + .. code-block:: python + + import numpy as np + + parts = visualizer.list_variables(dataset_id) + + # Update one variable on one specific part + part = parts[0] + var = part.variables[0] + + new_data = np.zeros((var.num_points, var.num_components)) + + visualizer.update_variables( + dataset_id, + [ + { + "type": var.type, + "name": var.name, + "num_components": var.num_components, + "data": new_data, + "part_id": part.part_id, + } + ], + ) + + .. tab-item:: HTTP API + + .. code-block:: python + + import requests, numpy as np + + payload = { + "variables": [ + { + "type": "point", + "name": "temperature", + "num_components": 1, + "data": [0.0] * 962, + "part_id": 1234567890, + } + ] + } + requests.post(f"http://localhost:53211/{dataset_id}/update_variables", json=payload) + + +Broadcast update (composite datasets) +-------------------------------------- + +Omit ``part_id`` (or set it to ``null``) on a composite dataset to broadcast +the same data to every part where the variable name exists and the array length +matches. The server skips parts where the variable name is not found or where +the array length does not match, and logs a warning. Use this mode to reset a +variable to a uniform value across all parts. + +.. tab-set:: + + .. tab-item:: Python + + .. code-block:: python + + import numpy as np + + parts = visualizer.list_variables(dataset_id) + var = parts[0].variables[0] # pick a variable present on all parts + + reset_data = np.zeros((var.num_points, var.num_components)) + + # No part_id: broadcasts to all matching parts + visualizer.update_variables( + dataset_id, + [ + { + "type": var.type, + "name": var.name, + "num_components": var.num_components, + "data": reset_data, + } + ], + ) + + .. tab-item:: HTTP API + + .. code-block:: python + + import requests + + payload = { + "variables": [ + { + "type": "point", + "name": "temperature", + "num_components": 1, + "data": [0.0] * 962, + # no "part_id" key: broadcast + } + ] + } + requests.post(f"http://localhost:53211/{dataset_id}/update_variables", json=payload) + +.. warning:: + + Broadcast applies the **same** data array to every matching part. Use it for + resets or uniform values, but not for time-stepping scenarios where each + part carries different data at each step. For those cases, issue one + targeted update (with ``part_id``) per part. + + +Non-composite dataset updates +------------------------------ + +For non-composite datasets, (``vtkPolyData``, ``vtkUnstructuredGrid``), +``part_id`` is optional and has no effect. + +.. tab-set:: + + .. tab-item:: Python + + .. code-block:: python + + import numpy as np + + parts = visualizer.list_variables(dataset_id) + var = parts[0].variables[0] # single-part dataset: parts[0] is the whole dataset + + new_data = np.random.rand(var.num_points * var.num_components) + + visualizer.update_variables( + dataset_id, + [ + { + "type": var.type, + "name": var.name, + "num_components": var.num_components, + "data": new_data, + # part_id not required for non-composite datasets + } + ], + ) + + .. tab-item:: HTTP API + + .. code-block:: python + + import requests + + payload = { + "variables": [ + { + "type": "point", + "name": "temperature", + "num_components": 1, + "data": [300.0, 305.5, 310.2], + } + ] + } + requests.post(f"http://localhost:53211/{dataset_id}/update_variables", json=payload) diff --git a/doc/source/user_guide/visualizer_ui/grid.rst b/doc/source/user_guide/visualizer_ui/grid.rst new file mode 100644 index 00000000..3e686631 --- /dev/null +++ b/doc/source/user_guide/visualizer_ui/grid.rst @@ -0,0 +1,17 @@ + +VISOR's UI, which exists as a layer above the VTK rendering window, is divided into panels. +The following linked pages describe how to use these panels. + +- :ref:`Rendering window <visor-rendering-window>` + Learn how to use the VISOR rendering window. + +- :ref:`Lower horizontal bar <visor-lower-horizontal-bar>` + Learn how to use the controls in the lower horizontal bar. + +- :ref:`Part list <visor-part-list>` + Learn how to use the part list. + +- :ref:`Top right panel <visor-top-right-panel>` + Learn how to use the **Part Properties** and **Legend settings** tabs. + +.. image:: /user_guide/visualizer_ui/images/ui-overview.png \ No newline at end of file diff --git a/doc/source/user_guide/visualizer_ui/images/bottom-overview.png b/doc/source/user_guide/visualizer_ui/images/bottom-overview.png new file mode 100644 index 00000000..1132dded Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/bottom-overview.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/bottom.png b/doc/source/user_guide/visualizer_ui/images/bottom.png new file mode 100644 index 00000000..774629ba Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/bottom.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/bounding-box.png b/doc/source/user_guide/visualizer_ui/images/bounding-box.png new file mode 100644 index 00000000..9c6e3502 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/bounding-box.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/cross-section.png b/doc/source/user_guide/visualizer_ui/images/cross-section.png new file mode 100644 index 00000000..f0c96804 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/cross-section.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/legend-settings.png b/doc/source/user_guide/visualizer_ui/images/legend-settings.png new file mode 100644 index 00000000..b9d01e9e Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/legend-settings.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/orthographic-mode.png b/doc/source/user_guide/visualizer_ui/images/orthographic-mode.png new file mode 100644 index 00000000..3fd0f708 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/orthographic-mode.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/part-list.png b/doc/source/user_guide/visualizer_ui/images/part-list.png new file mode 100644 index 00000000..7db1f378 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/part-list.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/part-properties.png b/doc/source/user_guide/visualizer_ui/images/part-properties.png new file mode 100644 index 00000000..d339d6c4 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/part-properties.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/perspective-mode.png b/doc/source/user_guide/visualizer_ui/images/perspective-mode.png new file mode 100644 index 00000000..f98a221d Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/perspective-mode.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/rendering-window.png b/doc/source/user_guide/visualizer_ui/images/rendering-window.png new file mode 100644 index 00000000..ca55ad60 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/rendering-window.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/top-right-panel.png b/doc/source/user_guide/visualizer_ui/images/top-right-panel.png new file mode 100644 index 00000000..a3945718 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/top-right-panel.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/ui-overview.png b/doc/source/user_guide/visualizer_ui/images/ui-overview.png new file mode 100644 index 00000000..8092e576 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/ui-overview.png differ diff --git a/doc/source/user_guide/visualizer_ui/images/wireframe.png b/doc/source/user_guide/visualizer_ui/images/wireframe.png new file mode 100644 index 00000000..9565b904 Binary files /dev/null and b/doc/source/user_guide/visualizer_ui/images/wireframe.png differ diff --git a/doc/source/user_guide/visualizer_ui/index.rst b/doc/source/user_guide/visualizer_ui/index.rst new file mode 100644 index 00000000..4e64f5d0 --- /dev/null +++ b/doc/source/user_guide/visualizer_ui/index.rst @@ -0,0 +1,19 @@ +.. _visualizer-ui-overview: + +Understand the VISOR UI +####################### + + +.. include:: grid.rst + + + +.. toctree:: + :maxdepth: 2 + :hidden: + + rendering_window + lower_horizontal_bar + part_list + top_right_panel + diff --git a/doc/source/user_guide/visualizer_ui/lower_horizontal_bar.rst b/doc/source/user_guide/visualizer_ui/lower_horizontal_bar.rst new file mode 100644 index 00000000..c4c4baea --- /dev/null +++ b/doc/source/user_guide/visualizer_ui/lower_horizontal_bar.rst @@ -0,0 +1,50 @@ +.. _visor-lower-horizontal-bar: + +Lower horizontal bar +#################### + +Use the lower horizontal bar to toggle scene widgets and fullscreen mode. + +.. image:: /user_guide/visualizer_ui/images/bottom-overview.png + +.. image:: /user_guide/visualizer_ui/images/bottom.png + +Use the following controls in the lower horizontal bar: + +- **Perspective/orthographic** + + - By default, VISOR opens models in perspective mode and hides the scale. + - Click the camera icon to switch to orthographic mode. The view changes and the scale is displayed. + - Click the camera icon again to return to perspective mode. + +.. image:: /user_guide/visualizer_ui/images/perspective-mode.png + +.. image:: /user_guide/visualizer_ui/images/orthographic-mode.png + +- **Cross-section** + + - Click the diamond icon to toggle the cross-section widget. + - When enabled, the widget bounds fit the full geometry bounding box. + - Use the 2D plane representation inside the widget bounds to change the cutting-plane location and orientation. + +.. image:: /user_guide/visualizer_ui/images/cross-section.png + +- **Edges/wireframe** + + - Click the grid-cube icon to toggle wireframe on or off. + - Wireframe applies globally and does not apply per part. + +.. image:: /user_guide/visualizer_ui/images/wireframe.png + +- **Fullscreen** + + - Click the popout-square icon to toggle fullscreen mode. + +- **Bounding box** + + - Click the bounded-cube icon to toggle the bounding-box widget. + - When enabled, a bounding box appears around the parts in the rendering window. + - The corners display the numeric bounds for the X, Y, and Z axes. + - The bounding-box size does not update when you hide parts. This behavior may change in a future VISOR release. + +.. image:: /user_guide/visualizer_ui/images/bounding-box.png diff --git a/doc/source/user_guide/visualizer_ui/part_list.rst b/doc/source/user_guide/visualizer_ui/part_list.rst new file mode 100644 index 00000000..ad985685 --- /dev/null +++ b/doc/source/user_guide/visualizer_ui/part_list.rst @@ -0,0 +1,42 @@ +.. _visor-part-list: + +Part list +######### + +Use the top-left panel to access the dataset part list. +This object tree helps you inspect parent-child relationships defined in the loaded VTK dataset. +You can search for a part, change visibility for individual parts or part groups, and select +individual parts for further actions. + +.. image:: /user_guide/visualizer_ui/images/part-list.png + +Use the following controls in the part list: + +- **Search input** + + - Type in the search input to filter the object tree by text. + - The filter matches part names. + +- **Show/hide** + + - Click the eye icon next to a part or part group name to hide it. + - Click the eye-slash icon to show it again. + +- **Part groups** + + - Click ``โ–ถ`` to collapse a part group. + - Click ``โ–ผ`` to expand a part group. + - The loaded VTK dataset defines which parts appear in each group. + - Part groups are organizational entries in the part list and do not exist as separate scene objects. + +- **Selecting parts** + + - Hold ``CTRL`` or ``SHIFT`` and click parts or part groups to select multiple items. + - Use ``CTRL`` + click to select individual parts or deselect a selected part. + - Use ``SHIFT`` + click to select a range of parts. + - Click a part group to select all descendant parts. + - Selected parts are shaded blue. + Parts with color variables or constants are not shaded when selected because of a VTK bug. + For related limitations, see :ref:`known-limitations`. + - If you select parts in the rendering window, the part list updates to match that selection. + - To clear all selections, left-click an empty area in the rendering window. \ No newline at end of file diff --git a/doc/source/user_guide/visualizer_ui/rendering_window.rst b/doc/source/user_guide/visualizer_ui/rendering_window.rst new file mode 100644 index 00000000..a8d4b1d9 --- /dev/null +++ b/doc/source/user_guide/visualizer_ui/rendering_window.rst @@ -0,0 +1,48 @@ +.. _visor-rendering-window: + +Rendering window +################ + +The rendering window is the scene behind the UI overlay. +Technically, it is an HTML ``<canvas>`` element where the frontend VTK rendering framework draws. +The rendering window shows the loaded geometry, widgets, and other items based on the current UI state. +You can also interact with the rendering window by using mouse and keyboard controls. + +.. image:: /user_guide/visualizer_ui/images/rendering-window.png + +Use the following controls and overlays in the rendering window: + +- **Camera hotkeys** + + - **Fit to camera**: Press ``Z`` to center the mesh in the window without rotating the camera. + - **Reset camera**: Press ``R`` to center the mesh and rotate the camera toward positive Z. + +- **Navigation controls** + + - **Rotate camera**: Hold the left mouse button and drag, or hold the middle mouse button and drag. + - **Pan**: Hold the left and right mouse buttons at the same time and drag. + - **Zoom**: Hold the right mouse button and drag up to zoom in or down to zoom out. + +- **Triad (rotation widget)** + + - The triad always matches the camera orientation. + - To snap to a specific axis, click ``X``, ``Y``, or ``Z`` in the widget. + This snap-to-axis behavior rotates the camera around a floating pivot in the center of the viewport, + not around the model bounding-box center. + +- **Scale** + + - The scale (ruler) overlay shows the current camera zoom scale. + - To set the displayed unit, provide a unit in the start options. + For more information, see :ref:`visor-metadata`. + - If you do not provide a unit in the start options, the scale overlay is not displayed. + - The scale overlay appears only in orthographic mode. + For more information, see :ref:`visor-lower-horizontal-bar`. + +- **Legend** + + - The legend overlay appears only when the selected part has a color variable (data array). + - The legend content changes based on the selected color variable, component, and minimum and + maximum values. + - You cannot control legend position or visibility. + - You cannot edit the displayed color gradient. \ No newline at end of file diff --git a/doc/source/user_guide/visualizer_ui/top_right_panel.rst b/doc/source/user_guide/visualizer_ui/top_right_panel.rst new file mode 100644 index 00000000..fcfd88fd --- /dev/null +++ b/doc/source/user_guide/visualizer_ui/top_right_panel.rst @@ -0,0 +1,94 @@ +.. _visor-top-right-panel: + +Top right panel +############### + +Use the top-right panel to modify the currently selected parts. +You can change transparency, diffuse color, and color variables (data arrays). +If you select multiple parts, the panel aggregates each field value. +When all selected parts share the same value, the panel shows that value. +When values differ, the panel shows an indicator. + +The panel has two tabs: **Part properties** and **Legend settings**. + +.. image:: /user_guide/visualizer_ui/images/top-right-panel.png + +Part properties +^^^^^^^^^^^^^^^ + +.. image:: /user_guide/visualizer_ui/images/part-properties.png + +Use the following fields on the **Part properties** tab: + +- **Name** + + - View the selected part name from the dataset (read-only). + +- **Opacity** + + - Drag the opacity slider left or right to change opacity. + - Use ``0%`` for fully transparent and ``100%`` for fully opaque. + - You can also enter an opacity value in the text field. + +- **Color variable** + + - Apply a data array (color variable) to map values to a color spectrum on the selected part. + - On this page, the terms *data array*, *color variable*, and *variable* have the same meaning. + - Open **Color by variable** to view available variables. + The list is the union of variables across selected parts. + If part A supports variable X and part B supports variable Y, the list shows X and Y. + The selected variable is applied only to parts that support it. + - Use scalar or multi-component arrays (for example, 2D, 3D, or 4D vectors). + - By default, VISOR uses **Magnitude** to compute color values. + In **Component**, choose a different component when available. + For example, scalar variables only show **Magnitude**, while 3D vectors can show + **Magnitude**, **X**, **Y**, and **Z**. + - Coloring by variable is part-specific. + - The legend overlay appears only when selected parts use the same variable and component. + - To disable variable coloring, select **Constant** and apply a solid diffuse color. + - Variables can be point-based or cell-based. See the following sections. + +Point-based variables +~~~~~~~~~~~~~~~~~~~~~~~ + +Point-based variables have the following behavior: + +- They store data at mesh points (vertices). +- They define values per point. +- During rendering, VTK interpolates values across cells formed by those points. +- Use them for smooth gradients such as temperature, pressure, or velocity at node locations. +- Example: If you assign temperature values to four points in a quad, VTK interpolates temperature across the surface. + +Cell-based variables +~~~~~~~~~~~~~~~~~~~~~~~ + +Cell-based variables have the following behavior: + +- They store data per cell (for example, triangles, quads, or tetrahedra). +- One value applies to the full cell. +- VTK does not interpolate values within the cell. +- Use them for material IDs, classifications, or other discontinuous per-cell properties. +- Example: If you assign a material type to each cell (for example, "metal" or "plastic"), that value applies to the full cell. + +Legend settings +^^^^^^^^^^^^^^^ + +.. image:: /user_guide/visualizer_ui/images/legend-settings.png + +Use the following fields on the **Legend settings** tab: + +- **Max (color variable maximum)** + + - Set the upper bound of the visible color spectrum for the current color variable. + - After you enter a value, press the **Enter** key or click **Apply**. + - Click **Reset** to restore the dataset's original maximum. + - This value applies globally to the color variable, not only to one part. + - If the value changes, the legend overlay updates. + +- **Min (color variable minimum)** + + - Set the lower bound of the visible color spectrum for the current color variable. + - After you enter a value, press the **Enter** key or click **Apply**. + - Click **Reset** to restore the dataset's original minimum. + - This value applies globally to the color variable, not only to one part. + - If the value changes, the legend overlay updates. diff --git a/doc/styles/.gitignore b/doc/styles/.gitignore new file mode 100644 index 00000000..72177bc1 --- /dev/null +++ b/doc/styles/.gitignore @@ -0,0 +1,4 @@ +* +!Vocab +!Vocab/** +!.gitignore diff --git a/doc/styles/Vocab/ANSYS/accept.txt b/doc/styles/Vocab/ANSYS/accept.txt new file mode 100644 index 00000000..61381a7b --- /dev/null +++ b/doc/styles/Vocab/ANSYS/accept.txt @@ -0,0 +1,48 @@ +[Aa]NSYS +Ansys +ansys +(?i)synopsys +api +API +available_resources +[Aa]utoscaling +[Aa]utoscale +[Aa]utoscaler +CLI +Ctrl +envs +extra_hosts +hostname +ipconfig +json +[Kk]eycloak +linux +mkcert +namespace +num_cores +num_instances +oauth +optiSLang +pcluster +postprocessing +portproxy +Pythonic +repadmin +repo +SAF +saf +TOCs +tocs +toc +traefik +ui +UI +wget +vm +VM +Autoscaling +hps +Synopsys +repId +ue +te diff --git a/doc/styles/Vocab/ANSYS/reject.txt b/doc/styles/Vocab/ANSYS/reject.txt new file mode 100644 index 00000000..e69de29b diff --git a/examples/assets/Pipe_ST_Discovery_Part_2.vtm b/examples/assets/Pipe_ST_Discovery_Part_2.vtm new file mode 100644 index 00000000..b9182022 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2.vtm @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<VTKFile type="vtkMultiBlockDataSet" version="1.0" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <vtkMultiBlockDataSet> + <DataSet index="0" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_0.vtp"/> + <DataSet index="1" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_1.vtp"/> + <DataSet index="2" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_2.vtp"/> + <DataSet index="3" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_3.vtp"/> + <DataSet index="4" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_4.vtp"/> + <DataSet index="5" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_5.vtp"/> + <DataSet index="6" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_6.vtp"/> + <DataSet index="7" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_7.vtp"/> + <DataSet index="8" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_8.vtp"/> + <DataSet index="9" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_9.vtp"/> + <DataSet index="10" file="Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_10.vtp"/> + </vtkMultiBlockDataSet> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_0.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_0.vtp new file mode 100644 index 00000000..f3ba36b1 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_0.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="856" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="858" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.2242742734" RangeMax="0.58600341296" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5608" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="5624" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5640" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="5656" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5672" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="5688" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5704" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="9556" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAABAUAAAWhAAAA==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AQAAAACAAADQGgAASAUAAA==eJw12MOiGAYARcHGtm3bbmw0tm1bbWzbjW3btm3bVhdvejfzDfcE+SNgwRmKYRmBkRmNMRmH8ZmISZmCqZmOGZmF2ZmLeZmfhViUJVia5ViBlVmNNVmH9dmITdmCrdmOHdmF3dmLfdmPAzmEwzmKYzmBkzmNMzmH87iQS7icq7iWG7iZ27iTe7ifh3iUJ3ia53iRV3idt3iXD/iYz/iSb/ien/iVP/ibgQMFGIwhGYbhGYlRGYOxGY8JmYTJmYppmYGZmY05mYd/siCLsDhLsSzLsxKrsgZrsx4bsgmbsxXbsgM7sxt7sg//4QAO5jCO5BiO5yRO5QzO5lwu4GIu40qu4Xpu4lbu4G7u40Ee4XGe4lle4GVe403e4X0+4lO+4Gu+40d+4Xf+YqDAAQZlCIZmOEZkFEZnLMZlAiZmMqZkGqZnJmZlDuZmPhZgYRZjSZbhX6zIKqzOWqzLBmzMZmzJNmzPTuzKHuzNv9mfgziUIzia4ziRUzids/gv53MRl3IFV3MdN3ILt3MX9/IAD/MYT/IMz/MSr/IGb/MeH/IJn/MV3/IDP/Mbf/KPIGBwhmJYRmBkRmNMxmF8JmJSpmBqpmNGZmF25mJe5mchFmUJlmY5VmBlVmNN1mF9NmJTtmBrtmNHdmF39mJf9uNADuFwjuJYTuBkTuNMzuE8LuQSLucqruUGbuY27uQe7uchHuUJnuY5XuQVXuct3uUDPuYzvuQbvucnfuUP/mbgoAEGY0iGYXhGYlTGYGzGY0ImYXKmYlpmYGZmY07m4Z8syCIszlIsy/KsxKqswdqsx4ZswuZsxbbswM7sxp7sw384gIM5jCM5huM5iVM5g7M5lwu4mMu4kmu4npu4lTu4m/t4kEd4nKd4lhd4mdd4k3d4n4/4lC/4mu/4kV/4nb8YKFiAQRmCoRmOERmF0RmLcZmAiZmMKZmG6ZmJWZmDuZmPBViYxViSZfgXK7IKq7MW67IBG7MZW7IN27MTu7IHe/Nv9ucgDuUIjuY4TuQUTucs/sv5XMSlXMHVXMeN3MLt3MW9PMDDPMaTPMPzvMSrvMHbvMeHfMLnfMW3/MDP/Maf/P/wB2FwhmJYRmBkRmNMxmF8JmJSpmBqpmNGZmF25mJe5mchFmUJlmY5VmBlVmNN1mF9NmJTtmBrtmNHdmF39mJf9uNADuFwjuJYTuBkTuNMzuE8LuQSLucqruUGbuY27uQe7uchHuUJnuY5XuQVXuct3uUDPuYzvuQbvucnfuUP/mbgEAEGY0iGYXhGYlTGYGzGY0ImYXKmYlpmYGZmY07m4Z8syCIszlIsy/KsxKqswdqsx4ZswuZsxbbswM7sxp7sw384gIM5jCM5huM5iVM5g7M5lwu4mMu4kmu4npu4lTu4m/t4kEd4nKd4lhd4mdd4k3d4n4/4lC/4mu/4kV/4nb8YKGSAQRmCoRmOERmF0RmLcZmAiZmMKZmG6ZmJWZmDuZmPBViYxViSZfgXK7IKq7MW67IBG7MZW7IN27MTu7IHe/Nv9ucgDuUIjuY4TuQUTucs/sv5XMSlXMHVXMeN3MLt3MW9PMDDPMaTPMPzvMSrvMHbvMeHfMLnfMW3/MDP/Maf/D/0B2FwhmJYRmBkRmNMxmF8JmJSpmBqpmNGZmF25mJe5mchFmUJlmY5VmBlVmNN1mF9NmJTtmBrtmNHdmF39mJf9uNADuFwjuJYTuBkTuNMzuE8LuQSLucqruUGbuY27uQe7uchHuUJnuY5XuQVXuct3uUDPuYzvuQbvucnfuUP/mbg0AEGY0iGYXj+B6SxuMI= + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_1.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_1.vtp new file mode 100644 index 00000000..5cb85a05 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_1.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="852" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="854" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.2242742734" RangeMax="0.58600341296" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5696" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="5712" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5728" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="5744" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5760" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="5776" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="5792" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="9644" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAADgTwAAnhAAAA==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 + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_10.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_10.vtp new file mode 100644 index 00000000..a789280c --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_10.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="164" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="160" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.53" RangeMax="0.57578207683" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1320" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1336" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1352" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1368" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1384" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1400" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1416" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="2228" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAABgDwAAywMAAA==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AQAAAACAAAAABQAAOgEAAA==eJwtxUFEGAAAAMDWbMrSNKXZbJrNUkopm2VZNstm2SylKaWU0iybpZRNszRLs5TNZlOapTRN02yW0pRNU5rSlKVsNsaIiIiIenT3uZ0B23Y72CHe632O8H4f9GEf8TFHO9bxTnSyTzrFqU7zOaf7ojN8xZnO9jXnucBFLnGZr7vCt1zpatf6ru+53g/c6Edudquf+rlfut2v3Oluv/Fbv/N7f/SAh/zZo/7qb57wd0/7h+c870X/9l//838vedkrXvWa173hTQfu2H6Xg7zHoQ5zuCN9wIcc5aM+7hjHOcFJPuFTPu0zPuvzvuBLvuyrznKOc53vQhe71OW+4Zu+7SrX+I7rfN8NfugmP3aLn/iZX7jNHX7tLve4133u9wd/8qCHPeIvHvO4Jz3lGc/6pxf8y3+8BQx/TDw= + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_2.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_2.vtp new file mode 100644 index 00000000..3ffcea04 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_2.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="154" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="150" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.26602365294" RangeMax="0.30567957079" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1056" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1072" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1088" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1104" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1120" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1136" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1152" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1820" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAABwDgAABgMAAA==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 + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_3.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_3.vtp new file mode 100644 index 00000000..8f674092 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_3.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="156" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="152" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.26602365294" RangeMax="0.30567957079" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1088" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1104" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1120" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1136" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1152" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1168" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1184" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1860" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAACgDgAAHQMAAA==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 + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_4.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_4.vtp new file mode 100644 index 00000000..21373e48 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_4.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="154" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="150" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.26602365294" RangeMax="0.30567957079" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1084" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1100" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1116" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1132" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1148" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1164" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1180" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1844" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAABwDgAAGwMAAA==eJyVlltIVGEUhUHChzIxyAqJQCF66iEIrBBPYZl0IXwoyMjsgmFmPSiFJaNRUVPiPDRQYtoFAkckTQosrVMadHnoovlUTFZW0FAUQRkKRnP+NX8sWRz08eOw/Wfvtdderesa2ou+vHDGIz8GNu667OxpyZ6/uXK/kz8j/U3+rJcJXj/7+tp3R044eZ3PIjeOWv6w8d9ftZN8oe9+WcTyFStTNm29eNwJjCzdW1Fk+c3S4ZGGkpBzYHTblaTPlo99r2oqTj3t3A1sbxrrtTxz0e/0rtxzTnjetC2piwcSvCP+f4OT+LH4O8POnWXt0zMqLR839cu72tIymy3PMvWzGtfEujotd8z7IxsGV3dMWP7EvP9j3fKk8qjlr0wf3i/8+XyoYDDBG03fCm5/vXTvl/0+t//qkvTHO526T9UTDw7a78+b/r/+E9jXW2X5t87st2nhUzlcp6YvXsflOrea43Vcfuch753uB3pnofdOl/swx+uD20Z9OOv1we2hPj/1+uxyn2d6fXZ5XkFvjpN4hTdHl3UyZOqzTlCfdYj315IOW8z7Wc81pj+s/zLTn+HDGSnFuy0Pmf630h5FTf8frc8Orir5r861juRQrDaH68w1esBcuhecjBV2tyTmgvmCY77QAzj0AP2A74jW542Wnsnh71mH4NAh9APOOgeHzqEfcN4jcOwR9hSc9xQ8i3wAnH0AHD4A/YCzz4DDZ6AfcPYxcPgY9AMOP4R+wOGfmDs45g79gMOH+XvWGzj0xu9hPYNDz/x7eV/AsS/cT95HcN53cN53cOx7D+mB/YTrs97Yr8DhV6xn+KHSv9oj5c/Kh9X+qn1X/qx+l/Jn1Tc/f+a5KH9Wc1f+rHSl/FnpVvmz2gvlz2rv+HvORbzXKi8p31C5SPmSXy5i31O5SPmqykXKt1UuUndB5SK1d1PNRconlR+qu6/uu98dZz9X913dC79czfdI5W1171Te9runnLfVvVZ5W+UBlbdV3lB5W+UZlbdVXlK5Wt0LlavVPVK5Wt07lav97innanWvVa5WeUDl6qnu11+OxYrgAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAQAAAACAAAAQDgAA3wEAAA==eJxd01tXjWEYhtGlJEkiSVJZUkmSJEk7SZIQSZIkSZIkZNNekiRJkvBvHfR9J/M9ucc1x3P6RiKbb0uwMWzosfRWPBaPo7fhcXg8vR2PxxPoHXgCnkjvxBPxJHoXnoQn07vxsPfgyXgKvRdPwVPpfXgqnkbvx9PwdPoAno5n0AfxDDyTzsIz8Wz6EJ6NR+nDeBTPoY/gOXguHcFz8Tw6Bs8P9miwBfQxvAAvpI/jhXgRfQIvwovpk3gxXkKfwkvwUvo0XoqX0WfwMrycPouX4xX0ObwCr6Sr8Eq8mq7Bq/Fa+jxei9fRF/A6vJ6+iNfjDfQlvAFvpC/jjXgTfQVvwpvpfLwZD//P1WCvBXudDb2FvoGHfRNvwVvpW3gr3kbfxtvwdvoO3o530HfxDryTvod34l30fbwL76Yf4N14D/0Q78F76Ud4L95HP8b78H76Cd6PD9BP8QF8kH6GD+JD9HN8CB+mX+DD+Aj9Eh/BX9Gv8dFg3wT7ln3HXdjv8VF8jB7Hx/AJehKfwKfoaXwKn6E/4DP4LP0Rn8Xn6E/4HD5Pf8bn8QX6C76AL9Jf8UV8if6GL+HL9Hd8GV+hf+Ar+Cr9E1/F1+hf+Bq+Tv/G1/EN+g++gf+l/+H/AWm2hIg=AQAAAACAAACwBAAAJgEAAA==eJwtxUFEGAAAAMBqSlnKpmma0jRLaZZSyqaUUsrSbJqmLM1SSlOaZbNZSimlNM1SSimbZilNszRNY4yIiBExRkSMMSLq0d3nzgScCnGYwx3p877gi77kOF/2FSc62dec6nRnOts3net8F7rYpS7zbd/1Pd93tWv80HVucJMfu9VP3O7nfukOd7nHfR7wkF/7jUc97klPe9bv/cHzXvQnf/aKv/qbv/uHf3rDm97ytn95x7v+7T/e874P/Nf//N+HPvKxgwJPD3aozzrC5xzlaMc41vFO8FUnOcXXneYMZ/mGc5znAhe5xLdc7juucKWr/MC1fuR6N7rZLW7zUz/zC79yp7vd634PetgjfusxT3jKM37nOX/0gpe87C9e9ZrXfQJpmEP5 + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_5.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_5.vtp new file mode 100644 index 00000000..9de95c9c --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_5.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="156" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="152" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.26602365294" RangeMax="0.30567957079" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1088" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1104" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1120" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1136" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1152" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1168" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="1184" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1856" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAACgDgAAHQMAAA==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 + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_6.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_6.vtp new file mode 100644 index 00000000..28513b06 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_6.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="113" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="96" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.2685925379" RangeMax="0.30423354284" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="752" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="768" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="784" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="800" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="816" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="832" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="848" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1360" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAACYCgAAIQIAAA==eJyVlksoRGEUxyVZMBGlRFmIlJKs0KjPI0NJslFS8lhJ2VDChqTMRikL0xjjkcXIwmOBIVc2ZEFeOyQlr5RHSbEQ9/59daZ/19zlr9P/O98553/u5/R/Jr4Gj43U3IuM6IE5tZo29Fi7NmI8jTo82f6jP74i+Ffg5bi6ZeqPOy0dcKazZnIldcBdsUnnrgTNI36/piJw6IBfZ74dnlWeSL7pJPdieUJHnLvJ4seSo+ricjT3en4+txEsWIhJ6dT87rnL2xg3TOvW8dEwHXmr+a0Vf9NfGNl+qXXmf/V7jMr1J//Wu+YPOzN5SXvNNP90T/nj8qKOzy521NRPDIboHJo6Sp47Zp6rSB2U1M8y9ZWsj8+sj5L3vTfvS+dhQ9Tz0oqPHt/ZbgscyXupssWDwFKf5rNW/myurrpTHI2tmp9bddityneXNGnuezntLY2vCInft+ovz/VZ/ZJ5WvUxoGPec9LAuZJD/7/5QIfVARz64Kye4IgHZ30ERzw4mwdwxIOzvoMjHpzNITjiwdmcgyMenPkFHPGSS5/KeHDmd5kPOPO1vC8420tBUU9wtt9kv8DZHpPzAM58IecNnPlLzjM482m4/mL/L/J/oXuS7Tc7fzEfgZt7rLjIzkfSL6wObC+xvWHXX9ZH2S+7uWXzKefQzo/Md8xfbG/I/RDmO0SxuWLvHPaeYe80di7LM8z3T0j+OJflz/6/7F30Da36nqs=AAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAQAAAACAAAAACQAAbgEAAA==eJx10kdPQmEURVF7pYhYQFRsWBEVEbEhIiL23rvY+/8fOfDcgTt5TE72ypebF2Npyd+vrOT/z7ocXqmt0lagq+G12jptDboe7tG6tG60F+6GN2gbtX6tD+2C++BNaA/c7jfjrnUL3A9vRQfgbdqQNohuhQfh7egA3O534K51JzwED6O74GF4N7YH76x7He6Ye9F9cC/c/h8i2n7tAHYQ76yH4BH4MHoEPgyPokfhUXgMPQaPwcfRE/BxeBw9CY/DE+gpeAKeRE/Dk/AUegaegs+i5+Cz8Hn0AnwenkYvwtPwDHoJnoFn0cvwLDyHXoHn4Hn0KjwPL6DX4AX4OnoDvqXd1m6id+C72j3tPvoAvg8/1B5pj/HO+sThjrl9x6n2DO+szx3umNt3XGgv8c76yuGOuX3HtfYG76xvHe6Y23fY3+kO76zvHe6Y23c8aB+1RfQTvAh/Rr/A37Tv2lf0B/xL+639RP/AfwEpaT12AQAAAACAAAAAAwAAyAAAAA==eJwtxVtEAwAAAMAeltKUTWlKKSIiRsSIMcYYERExYoyIiIgYY0RERESMETEiIiIiIiJGjBgjIsaIiKiP3f1cd0dbj/sc9KDDHnbEY57wlKc941nPOep5LzjmRcedcNIpp73kZa941WvOeN1Z57zhTW952zve9Z7zLrjofR/40Ec+9olPfeaSyz73hSu+9JWvfeNb3/neD370k5/94qpfXfOb62743R/+dNMtf/nbP/71n7s62wfc634POOQhj3jU4570P6sgK5w= + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_7.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_7.vtp new file mode 100644 index 00000000..9cffca72 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_7.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="113" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="96" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.2685925379" RangeMax="0.30423354284" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="800" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="816" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="832" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="848" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="864" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="880" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="896" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1408" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAACYCgAARQIAAA==eJytVkFIVFEUhTFmYcOAoiCGMohughBBsGhxjVAGRMSNIIGV4UKSEJUiIlAiyI0iunBQHC0cGHBidCFq5lcTcqX8n+JGK4KIEKExkEgXod/zwwuHZ9Asz1zOv/fdc857ifznu7UzPXKpeKfQ3zUu16OHmftzjnx3ZOthxdN54F+Kfq5vhj94+FE85VQ3jnp44pQHuIknQb77WuF7fYHI5agtIy1pmTuxJo+n8mL2dmWGLUtuvaV53iocPKHyQE398DOZOfl/xPIPLi82x2359KNjqCH4wsMf/Lo15vtmy/BKKNkWaJsHPpBzoS54xZH+yPGvWzS+oXjeXJ1Iz213JLX8siR79a4F/POj3EDDPVumXVw03nvC/9jDbybX4pNPbEm5/Jbus8Sdy9JzTbo8luYpVfUFkYrdqaQj+4p/7rT/cXdeS88bU/2HZ/eiCweOZL07Oy/wBdXP185rvvsfHclTe0E/neocUP+vewcOfQJfUrhJt+y7Jh7tF7Z3phPf4u3fr8KtHs/7qrLuG3f+6ge41g9w7J3pCr5APfRj8gvqoUPmI/gF9ef1EerhI+avPNU/9GPSFeqhK6a3mDpn6Nmkf11v8oXuh/kF/tXzmnyNevja5He9L5YDKcWPvZvySuvNlGNazyzf4CPti//lL1aPewo47inmR9x3OpeYH9lc7HxY7rHzZ3tkemC5x/TGdM78wvKT6Z/5muUDe7cwv7NcYvnGcpK9c5gemK7Yu4jxs3cXOwfWD7tnWT+sf6bPP7S6kCQ=AAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAQAAAACAAAAACQAAbAEAAA==eJx10UdPgkEYRWELduy9VyyoiIiI2BARsQF2UETsvfz/lQvvu/AkH5ub82SYTKC05O9TVvL/Y10Or9BWal3oKrgLXo2ugddq67RudD3cDW/QNmqbcM662eEec3tHi7YV56zbHO4xt3e0aztwzrrT4R5ze0eXthvnrHsc7jG3d9jv1Itz1n0O95jbOwa0g9p+9BB8RDuqHUaPwT3ace0EdhLnrKfgHrgXPQ33wmfQs/AZuA89B/fB/eh5uB8eQC/AA/AgehEehIfQS/AQPIxehofhEfQKPAJfRa/BV+Hr6A34OjyK3oRH4TH0FjwGj6O34XF4Ar0DT8CT6F14Er6H3ocfaA+1Kbh1Gp2Bp+FHuPcY37c+gafgp+gzuPU53PoCfgrPonPwK21ee4nOwi/h1+gc/EZb1BbQeXgBfou+gtv9d7jX+h5ehGfQD/AM3P73J+2z9hH9An/Tvmtf0R/wL+239hP9A/8F88c+yg==AQAAAACAAAAAAwAAyAAAAA==eJwtxVtEAwAAAMAeltKUTWlKKSIiRsSIMcYYERExYoyIiIgYY0RERESMETEiIiIiIiJGjBgjIsaIiKiP3f1cd0dbj/sc9KDDHnbEY57wlKc941nPOep5LzjmRcedcNIpp73kZa941WvOeN1Z57zhTW952zve9Z7zLrjofR/40Ec+9olPfeaSyz73hSu+9JWvfeNb3/neD370k5/94qpfXfOb62743R/+dNMtf/nbP/71n7s62wfc634POOQhj3jU4570P6sgK5w= + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_8.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_8.vtp new file mode 100644 index 00000000..ab55f1b3 --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_8.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="113" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="96" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.2685925379" RangeMax="0.30423354284" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="800" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="816" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="832" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="848" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="864" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="880" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="896" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1412" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAACYCgAARgIAAA==eJyVllFI01EYxUHFh1xGghpLIsReBJGeLDe6RTiEkPBFkEA0n6LowcIQDRwjaC9K0IND2UqYMAhaPqlBEx0aIoor6SUlBBERQQ1CcA/h/vfcC58c/nNv/x+H797v3vOdO1/spOxoJqOu1m/UFAfjavzam73WqaHU1o2/q+vNPwzfzahfvU1hP3g2cZhpefzBcJ+uA+5Wx0fW3X/nidTG1gyPax4oKf8duGx59Flh2cbE6zN1lh29knUWND+vHhx9yTrgrE5c9AW97AscfbF+2T6ZntVnff155fV0dK+pqdx3NDU8N36z/HtXSvLK+RxXiw8awvc6bZ2qSxerR6aDfujBUed+ciXxpd/ygcjpr89w1AcvHpmbfZKw+oq7noftYyHDoQd/fvzoY8GO1a8fvBztKH1rOPTg768UtZXW2XsM59YNGw49+Ndbny54X1j9kq4DDj14daRpbzKZObN/cLn/7cHbBU83rb5XnwM49ODN0/uxb/+sfkCfM7g5T8Ghv6PvUerB5X5+OusquR9w2a9y+lKyX/AZcZ5Z59wMhx5c3tdn516UvC9w6YcTXUf6AVz6rVHvU/oNXPo5rc9B+hlczgXm6LzzxXKG5SrLZ5ZLbC7YfLE6bF02R8xvzLf5vkfIZ+ZPlofMP8yHzM9sTtm8s9xgechyleU/y+d88xz+ZP7Pd45kbrN8Bh9LX0/2eEJ+t3yWOczmi7137D1yyw2WDzIH3PKQ5Z7MN7ecZ3nOcpu9R/Ldyfd/HeaOzSOb3/+1zYASAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAQAAAACAAAAACQAAcQEAAA==eJx10UdPQlEURWEL9t6wi4iIiIiIiF0RRBC72LH37v8fOXCfgSt5THbWl5ubm0dpyd+vrOT/z7ocXqGt1LrQVXAXvBpdA6/T1mtr0Q3wJm2zthHdAm/Ttmtb0R1wt7ZT24XtxjnrHrgb3ovug/fC+9ED8H64Bz0I98C96CG4F+5DD8N9cD96BO6HB9Cj8AA8iB6DB+Eh9Dg8BA+jJ+BheAQ9CY/Ao+gpeBQeQ0/DY/A4egYehyfQs/AEfA49D5+DL6AX4cvaFe0SehW+pk1pk+g0PAlf125os9oMOgfPwDe1ee2WdhtunUPvwHNwu9/euYv3We/Bs/A0eh+ehtv3se98oE2hD+EpeAF9BC/Aj7EnOGd96nCPeR59Bs/D7X87115oi+hLeBF+pb3W3uCc9a3DPeb2jjvtPc5ZPzjcY27veNQ+4Zz1s8M95vaOF+0rzlm/Odxjbu+w7/SOc9YfDveY2zu+tN/aT/QP/BdbnUH5AQAAAACAAAAAAwAAyAAAAA==eJwtxVtEAwAAAMAeltKUTWlKKSIiRsSIMcYYERExYoyIiIgYY0RERESMETEiIiIiIiJGjBgjIsaIiKiP3f1cd0dbj/sc9KDDHnbEY57wlKc941nPOep5LzjmRcedcNIpp73kZa941WvOeN1Z57zhTW952zve9Z7zLrjofR/40Ec+9olPfeaSyz73hSu+9JWvfeNb3/neD370k5/94qpfXfOb62743R/+dNMtf/nbP/71n7s62wfc634POOQhj3jU4570P6sgK5w= + </AppendedData> +</VTKFile> diff --git a/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_9.vtp b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_9.vtp new file mode 100644 index 00000000..b68568eb --- /dev/null +++ b/examples/assets/Pipe_ST_Discovery_Part_2/Pipe_ST_Discovery_Part_2_9.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="113" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="96" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float64" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.2685925379" RangeMax="0.30423354284" offset="0" /> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="820" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="836" /> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="852" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="868" /> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="884" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="900" /> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="916" /> + <DataArray type="Int64" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="1428" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAACYCgAAUwIAAA==eJyVVk1IVFEYBRkGyUFQEMogSNSFEOLGitSXqIMhKrUIQhDNzaRQiwQJF6UiKFIw4MJBaVRwMeBCXfkD3bBFLfqZ9yJxUUMIUiiClkhQi2jG84QTh4tvefg493z3nu98L3lhePfm8jPnfPmX4uDAnLMXDcXK4q7piw6+K6keqQIezin4HM47wf8kDrzmu9M+njzmAQ6eyQxumB841yt+pSd0PdR6Z2rILKf5npvgxPrLewnXvN3vnWzPHfHx+7/aZrK+uWantCJ7Pv9pFfDxs4HbuZc88zj27xs1jBviWbsyf6bwoWeqX81WFLzpdIB/7SsMtXe5pns9jRvGI2n+Rz5et/A+sdjvmsBBmt9hnfWZvhzuazPD4zDPDaovijXsLi14Jkj8q8f6VzL9OtzvLdLfuLIXf3HkmQj1C/w76dl+cjWrJ+X99y7Q00n3gHrlkwnCr8V/5/9Y9XwcPgGufLVBPkd9Uvhf+XmDcPCUpx5cPHcY9f25VfLzw6fGj1K/6lf5/LT6lR7FY+sLuK0vda6Nh8+Fr4DDJ/AhcPhQ8au+MC/AeV6AY14wF3wu/Mw6VV+YCz4X+QMc84gcA458UD43VL9G8878yucBugecy/qRSxG6B+SSyvNuuk/kZGXNTjjVMubXv266PFrb4fp5y/Wb5BOcW0/vC50qn1WeKz1Kv7oHlf/qPm17jd9F7TvlK9u+Y/+oPaj8bMt/nhe1F9Tc2fYU19v2F+tRe03lhm3/cp7Y9jK/l9rXak5t/xXsN9v/BvtZ/YeoOT3tfP0FCO2vXA==AAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAACAAAAAAAAAAQAAAACAAAAACQAAbQEAAA==eJx10NkzgmEYhnH7kn3f1+w72ZNCtixJkiRJSBL+/yMHnvvANfN1cs/1m3feeb9KS/5+ZSX/f+pyeKVtlW0FuhpeY1tr60LXwV3wetsG20acUzc53CPXO5ptW3BO3epwj1zvaLNtxzl1h8M9cr2j07YL59TdDvfI9Q79Tz04p+51uEeud/TbDtj2oQfhw7YjtkPoUfiY7bitGz0Bd8Mn0VPwGdtZ22n0HHzBdtF2Hr0EX7H12C6jV+HL8DX0Olz3b+Be9SbcA99Cb8N3bH22XvQW3AvfRW/D9T36Pj86APfD9/DOfbxPfQD3wYPoQ3gQfoQ9xjn1icM98gD6FB6A63tDtme259gLnFNfwkPwMPoKHoZH0NfwCDyKvoFH4TH0LTwGj6Pv4HF4An0PT8CT6Ad4Ep5CP8JT8DT6CZ6GZ9DP8Aw8i36BZ+Gv6Df4KzyHfofn4Hn0BzwPL6A/4QV4Ef0FL8K/0T/wXytqQpo=AQAAAACAAAAAAwAAyAAAAA==eJwtxVtEAwAAAMAeltKUTWlKKSIiRsSIMcYYERExYoyIiIgYY0RERESMETEiIiIiIiJGjBgjIsaIiKiP3f1cd0dbj/sc9KDDHnbEY57wlKc941nPOep5LzjmRcedcNIpp73kZa941WvOeN1Z57zhTW952zve9Z7zLrjofR/40Ec+9olPfeaSyz73hSu+9JWvfeNb3/neD370k5/94qpfXfOb62743R/+dNMtf/nbP/71n7s62wfc634POOQhj3jU4570P6sgK5w= + </AppendedData> +</VTKFile> diff --git a/examples/assets/tensors9.vtp b/examples/assets/tensors9.vtp new file mode 100644 index 00000000..bfc4b466 --- /dev/null +++ b/examples/assets/tensors9.vtp @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="9" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="8"> + <PointData Tensors="Tensors9"> + <DataArray type="Float64" Name="Tensors9" NumberOfComponents="9" format="binary" RangeMin="2.244994432064365" RangeMax="2.2715633383201097"> + AQAAAACAAACIAgAAmwAAAA==eJwz+f4fCL7bM4DBAygNAw/sz3CA6B9o4ggwEwxOQuU/2AeemTHDbXrhXpg8Gt8BlzkQcMH+LNQ+Roj9+2EyML4pinsxgTEYXIbJO7DHiBirLVu4ByaPyv+A0xyYe4hzN25z0N1DrjlpYHDN3gQ1vvajqnqwn1B8oboHHl+2MHk0Pk73wtyDFl9we2F8QvE1C5x+btoDAEQTYvs= + <InformationKey name="L2_NORM_RANGE" location="vtkDataArray" length="2"> + <Value index="0"> + 2.2449944321 + </Value> + <Value index="1"> + 2.2715633383 + </Value> + </InformationKey> + </DataArray> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="binary" RangeMin="0" RangeMax="2.8284271247461903"> + AQAAAACAAABsAAAAIQAAAA==eJxjYEAGDfZIHAcGrHIgGq7OAbsesJgDkhiYDQAoMQX7 + <InformationKey name="L2_NORM_RANGE" location="vtkDataArray" length="2"> + <Value index="0"> + 0 + </Value> + <Value index="1"> + 2.8284271247 + </Value> + </InformationKey> + </DataArray> + </Points> + <Verts> + <DataArray type="Int64" Name="connectivity" format="binary" RangeMin="1e+299" RangeMax="-1e+299"> + AAAAAACAAAAAAAAA + </DataArray> + <DataArray type="Int64" Name="offsets" format="binary" RangeMin="1e+299" RangeMax="-1e+299"> + AAAAAACAAAAAAAAA + </DataArray> + </Verts> + <Lines> + <DataArray type="Int64" Name="connectivity" format="binary" RangeMin="1e+299" RangeMax="-1e+299"> + AAAAAACAAAAAAAAA + </DataArray> + <DataArray type="Int64" Name="offsets" format="binary" RangeMin="1e+299" RangeMax="-1e+299"> + AAAAAACAAAAAAAAA + </DataArray> + </Lines> + <Strips> + <DataArray type="Int64" Name="connectivity" format="binary" RangeMin="1e+299" RangeMax="-1e+299"> + AAAAAACAAAAAAAAA + </DataArray> + <DataArray type="Int64" Name="offsets" format="binary" RangeMin="1e+299" RangeMax="-1e+299"> + AAAAAACAAAAAAAAA + </DataArray> + </Strips> + <Polys> + <DataArray type="Int64" Name="connectivity" format="binary" RangeMin="0" RangeMax="8"> + AQAAAACAAADAAAAAMgAAAA==eJxjYIAARijNwoAKYHxmNHVMUJoVTRzGR9cH47OjicP4bGjqYOZwoInD+DB9AB0gAGE= + </DataArray> + <DataArray type="Int64" Name="offsets" format="binary" RangeMin="3" RangeMax="24"> + AQAAAACAAABAAAAAHgAAAA==eJxjZoAANijNCaV5oDQ/lBaC0qJQWgJKAwALgABt + </DataArray> + </Polys> + </Piece> + </PolyData> +</VTKFile> diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json new file mode 100644 index 00000000..3678ce79 --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json @@ -0,0 +1,35 @@ +{ + "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z0", + "unit": "m", + "state": { + "parts": { + "level_1_block_0_part_0": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_1": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_2": { + "opacity": 0.1111111111111111 + }, + "level_1_block_1_part_0": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_1": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_2": { + "opacity": 0.2222222222222222 + }, + "level_1_block_2_part_0": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_1": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_2": { + "opacity": 0.3333333333333333 + } + } + } +} \ No newline at end of file diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm new file mode 100644 index 00000000..d67a4610 --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm @@ -0,0 +1,19 @@ +<VTKFile type="vtkMultiBlockDataSet" version="1.0" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <vtkMultiBlockDataSet> + <Block name="level_0_block_0_part_0_parent" index="0"> + <DataSet index="0" name="level_1_block_0_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp"/> + <DataSet index="1" name="level_1_block_0_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp"/> + <DataSet index="2" name="level_1_block_0_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp"/> + </Block> + <Block name="level_0_block_0_part_1_parent" index="1"> + <DataSet index="0" name="level_1_block_1_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp"/> + <DataSet index="1" name="level_1_block_1_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp"/> + <DataSet index="2" name="level_1_block_1_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp"/> + </Block> + <Block name="level_0_block_0_part_2_parent" index="2"> + <DataSet index="0" name="level_1_block_2_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp"/> + <DataSet index="1" name="level_1_block_2_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp"/> + <DataSet index="2" name="level_1_block_2_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp"/> + </Block> + </vtkMultiBlockDataSet> +</VTKFile> diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp new file mode 100644 index 00000000..63116026 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp new file mode 100644 index 00000000..6c7d713c Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp new file mode 100644 index 00000000..009ffd8f Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp new file mode 100644 index 00000000..fd7d6df8 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp new file mode 100644 index 00000000..3b536721 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp new file mode 100644 index 00000000..7addcb96 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp new file mode 100644 index 00000000..a497d8b6 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp new file mode 100644 index 00000000..1e93ea0e Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp new file mode 100644 index 00000000..f905e0cc Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json new file mode 100644 index 00000000..f5948b4e --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json @@ -0,0 +1,35 @@ +{ + "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z3", + "unit": "m", + "state": { + "parts": { + "level_1_block_0_part_0": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_1": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_2": { + "opacity": 0.1111111111111111 + }, + "level_1_block_1_part_0": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_1": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_2": { + "opacity": 0.2222222222222222 + }, + "level_1_block_2_part_0": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_1": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_2": { + "opacity": 0.3333333333333333 + } + } + } +} \ No newline at end of file diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm new file mode 100644 index 00000000..0bd10955 --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm @@ -0,0 +1,19 @@ +<VTKFile type="vtkMultiBlockDataSet" version="1.0" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <vtkMultiBlockDataSet> + <Block name="level_0_block_0_part_0_parent" index="0"> + <DataSet index="0" name="level_1_block_0_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_0.vtp"/> + <DataSet index="1" name="level_1_block_0_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_1.vtp"/> + <DataSet index="2" name="level_1_block_0_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_2.vtp"/> + </Block> + <Block name="level_0_block_0_part_1_parent" index="1"> + <DataSet index="0" name="level_1_block_1_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_3.vtp"/> + <DataSet index="1" name="level_1_block_1_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_4.vtp"/> + <DataSet index="2" name="level_1_block_1_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_5.vtp"/> + </Block> + <Block name="level_0_block_0_part_2_parent" index="2"> + <DataSet index="0" name="level_1_block_2_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_6.vtp"/> + <DataSet index="1" name="level_1_block_2_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_7.vtp"/> + <DataSet index="2" name="level_1_block_2_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_8.vtp"/> + </Block> + </vtkMultiBlockDataSet> +</VTKFile> diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json new file mode 100644 index 00000000..3678ce79 --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json @@ -0,0 +1,35 @@ +{ + "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z0", + "unit": "m", + "state": { + "parts": { + "level_1_block_0_part_0": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_1": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_2": { + "opacity": 0.1111111111111111 + }, + "level_1_block_1_part_0": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_1": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_2": { + "opacity": 0.2222222222222222 + }, + "level_1_block_2_part_0": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_1": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_2": { + "opacity": 0.3333333333333333 + } + } + } +} \ No newline at end of file diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm new file mode 100644 index 00000000..d67a4610 --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm @@ -0,0 +1,19 @@ +<VTKFile type="vtkMultiBlockDataSet" version="1.0" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <vtkMultiBlockDataSet> + <Block name="level_0_block_0_part_0_parent" index="0"> + <DataSet index="0" name="level_1_block_0_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp"/> + <DataSet index="1" name="level_1_block_0_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp"/> + <DataSet index="2" name="level_1_block_0_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp"/> + </Block> + <Block name="level_0_block_0_part_1_parent" index="1"> + <DataSet index="0" name="level_1_block_1_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp"/> + <DataSet index="1" name="level_1_block_1_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp"/> + <DataSet index="2" name="level_1_block_1_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp"/> + </Block> + <Block name="level_0_block_0_part_2_parent" index="2"> + <DataSet index="0" name="level_1_block_2_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp"/> + <DataSet index="1" name="level_1_block_2_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp"/> + <DataSet index="2" name="level_1_block_2_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp"/> + </Block> + </vtkMultiBlockDataSet> +</VTKFile> diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp new file mode 100644 index 00000000..63116026 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_0.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp new file mode 100644 index 00000000..6c7d713c Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp new file mode 100644 index 00000000..009ffd8f Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_2.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp new file mode 100644 index 00000000..fd7d6df8 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_3.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp new file mode 100644 index 00000000..3b536721 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_4.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp new file mode 100644 index 00000000..7addcb96 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_5.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp new file mode 100644 index 00000000..a497d8b6 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_6.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp new file mode 100644 index 00000000..1e93ea0e Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_7.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp new file mode 100644 index 00000000..f905e0cc Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z0/vtk_scene_sphere_l2_b3_r32_v3_c1_z0_8.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json new file mode 100644 index 00000000..f5948b4e --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json @@ -0,0 +1,35 @@ +{ + "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z3", + "unit": "m", + "state": { + "parts": { + "level_1_block_0_part_0": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_1": { + "opacity": 0.1111111111111111 + }, + "level_1_block_0_part_2": { + "opacity": 0.1111111111111111 + }, + "level_1_block_1_part_0": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_1": { + "opacity": 0.2222222222222222 + }, + "level_1_block_1_part_2": { + "opacity": 0.2222222222222222 + }, + "level_1_block_2_part_0": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_1": { + "opacity": 0.3333333333333333 + }, + "level_1_block_2_part_2": { + "opacity": 0.3333333333333333 + } + } + } +} \ No newline at end of file diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm new file mode 100644 index 00000000..0bd10955 --- /dev/null +++ b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm @@ -0,0 +1,19 @@ +<VTKFile type="vtkMultiBlockDataSet" version="1.0" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <vtkMultiBlockDataSet> + <Block name="level_0_block_0_part_0_parent" index="0"> + <DataSet index="0" name="level_1_block_0_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_0.vtp"/> + <DataSet index="1" name="level_1_block_0_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_1.vtp"/> + <DataSet index="2" name="level_1_block_0_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_2.vtp"/> + </Block> + <Block name="level_0_block_0_part_1_parent" index="1"> + <DataSet index="0" name="level_1_block_1_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_3.vtp"/> + <DataSet index="1" name="level_1_block_1_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_4.vtp"/> + <DataSet index="2" name="level_1_block_1_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_5.vtp"/> + </Block> + <Block name="level_0_block_0_part_2_parent" index="2"> + <DataSet index="0" name="level_1_block_2_part_0" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_6.vtp"/> + <DataSet index="1" name="level_1_block_2_part_1" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_7.vtp"/> + <DataSet index="2" name="level_1_block_2_part_2" file="vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_8.vtp"/> + </Block> + </vtkMultiBlockDataSet> +</VTKFile> diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_0.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_0.vtp new file mode 100644 index 00000000..5979405c Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_0.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_1.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_1.vtp new file mode 100644 index 00000000..1c584b39 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_1.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_2.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_2.vtp new file mode 100644 index 00000000..76215b52 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_2.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_3.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_3.vtp new file mode 100644 index 00000000..0f101b02 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_3.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_4.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_4.vtp new file mode 100644 index 00000000..f896a012 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_4.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_5.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_5.vtp new file mode 100644 index 00000000..93116873 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_5.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_6.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_6.vtp new file mode 100644 index 00000000..e4bfbd84 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_6.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_7.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_7.vtp new file mode 100644 index 00000000..9b180a7b Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_7.vtp differ diff --git a/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_8.vtp b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_8.vtp new file mode 100644 index 00000000..5d54f150 Binary files /dev/null and b/examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3/vtk_scene_sphere_l2_b3_r32_v3_c1_z3_8.vtp differ diff --git a/examples/notebooks/Variable Update Testing - Multiblock Example HTTP API.ipynb b/examples/notebooks/Variable Update Testing - Multiblock Example HTTP API.ipynb new file mode 100644 index 00000000..e8cb298d --- /dev/null +++ b/examples/notebooks/Variable Update Testing - Multiblock Example HTTP API.ipynb @@ -0,0 +1,325 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "1074e66d-b996-4bae-85d3-464550638651", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import numpy as np\n", + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e8b26455-710f-447d-b498-79b64e85b824", + "metadata": {}, + "outputs": [], + "source": [ + "import requests" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bcee0a20-9cb6-4424-b45c-11354e16a858", + "metadata": {}, + "outputs": [], + "source": [ + "# Start the Visor server on the command line using:\n", + "# visor-cli server start\n", + "\n", + "# Take note of the service endpoint and assign to the url variable here.\n", + "url = \"http://localhost:53211\"\n", + "\n", + "# Set up endpoints for the operations we will need\n", + "# Start\n", + "start_url = f\"{url}/start\"\n", + "# List datasets\n", + "list_datasets_url = f\"{url}/list_datasets\"\n", + "# List variables (for dataset)\n", + "def list_variables_url(dataset_id):\n", + " return f\"{url}/{dataset_id}/list_variables\"\n", + "# Update variables (for dataset)\n", + "def update_variables_url(dataset_id):\n", + " return f\"{url}/{dataset_id}/update_variables\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bdafd5f6-67c1-4d73-bedc-1f87fde9ad58", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the start url and payload\n", + "start_payload={\"file_path\": \"examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm\",\n", + " \"metadata\": \"examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json\"}\n", + "\n", + "# Submit the request\n", + "resp = requests.post(start_url, json=start_payload)\n", + "resp.content\n", + "dataset_id1 = resp.json().get(\"dataset_id\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e973dbd6-66e0-4f31-af8d-227047c5e81d", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1000\"\n", + " height=\"500\"\n", + " src=\"http://localhost:8081\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x1e6e1f43150>" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IFrame(\"http://localhost:8081\", width=\"1000\", height=\"500\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b1102db3-ff09-4955-ae71-f49270619483", + "metadata": {}, + "outputs": [], + "source": [ + "# Select the parent dataset in the tree view, and color by Normals" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8d661164-96a4-41de-8468-2cf4b5a6d802", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'2278968957650919': {'id': 2278968957650919,\n", + " 'name': 'vtk_scene_sphere_l2_b3_r32_v3_c1_z0',\n", + " 'unit': 'm',\n", + " 'file_path': 'examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm',\n", + " 'metadata_path': 'examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json'}}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List datasets and get the dataset ID\n", + "resp = requests.get(list_datasets_url)\n", + "datasets = resp.json().get('datasets', {})\n", + "datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "aef0fe4b-b016-4ca2-a87f-6ae446120f0f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'part_id': 3554123176990211,\n", + " 'part_name': 'level_1_block_2_part_2',\n", + " 'variables': [{'index': 0,\n", + " 'type': 'POINT',\n", + " 'name': 'Normals',\n", + " 'num_components': 3,\n", + " 'num_points': 962,\n", + " 'ranges': [[-0.9987165331840515, 0.9987165331840515],\n", + " [-0.9987165331840515, 0.9987165331840515],\n", + " [-1.0, 1.0]],\n", + " 'magnitude_range': [0.9999999680464912, 1.0000000418852286]},\n", + " {'index': 1,\n", + " 'type': 'POINT',\n", + " 'name': 'gradient_variable_0',\n", + " 'num_components': 1,\n", + " 'num_points': 962,\n", + " 'ranges': [[0.0, 1.0]],\n", + " 'magnitude_range': [0.0, 1.0]},\n", + " {'index': 2,\n", + " 'type': 'POINT',\n", + " 'name': 'constant_variable_1',\n", + " 'num_components': 1,\n", + " 'num_points': 962,\n", + " 'ranges': [[1.0, 1.0]],\n", + " 'magnitude_range': [1.0, 1.0]},\n", + " {'index': 3,\n", + " 'type': 'POINT',\n", + " 'name': 'random_variable_2',\n", + " 'num_components': 1,\n", + " 'num_points': 962,\n", + " 'ranges': [[0.0001607129815965891, 0.9997527003288269]],\n", + " 'magnitude_range': [0.0001607129815965891, 0.9997527003288269]}]}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List variables for dataset_id\n", + "resp = requests.get(list_variables_url(dataset_id1))\n", + "parts = resp.json().get('parts')\n", + "# Choose the last part to update\n", + "part_to_update = parts[-1]\n", + "part_id = part_to_update.get(\"part_id\")\n", + "variables = part_to_update.get(\"variables\")\n", + "part_to_update" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "1e7d2d55-03db-4856-b950-9c80788c1d48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Updating part level_1_block_2_part_2, variable Normals, POINT, 3\n" + ] + } + ], + "source": [ + "# Create the new variables for the test point dataArray (zeros):\n", + "var_to_update = variables[0]\n", + "name = var_to_update[\"name\"]\n", + "var_type = var_to_update[\"type\"]\n", + "num_points = var_to_update[\"num_points\"]\n", + "num_components = var_to_update[\"num_components\"]\n", + "\n", + "print(f\"Updating part {part_to_update.get('part_name')}, variable {name}, {var_type}, {num_components}\")\n", + "\n", + "new_vector_values = np.zeros((num_points, num_components))\n", + "# need to flatten to a python list of num_points*num_components values\n", + "new_vector_values = [0]*num_points*num_components" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a1f0d446-11d0-4ca0-b759-b6614d51adb5", + "metadata": {}, + "outputs": [], + "source": [ + "# Create new vector data\n", + "new_vector_values = np.zeros((num_points, num_components))\n", + "# need to flatten to a python list of num_points*num_components values\n", + "new_vector_values = new_vector_values.flatten().tolist()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "602431e0-1173-4676-a689-3f0d8e671ff3", + "metadata": {}, + "outputs": [], + "source": [ + "# Compile a dict with the vector variable metadtata + updated values\n", + "vector_update_info = {\n", + " \"type\": \"point\",\n", + " \"name\": name,\n", + " \"num_components\": num_components,\n", + " \"data\": new_vector_values,\n", + " \"part_id\": part_id\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4b1c0fc9-d7df-454c-884f-0566a35585a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the actual update command\n", + "payload = {\"variables\": [vector_update_info]}\n", + "resp = requests.post(update_variables_url(dataset_id1), json=payload)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df3ddc5d-0fa5-470e-a046-09f707cb1cc6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2015384d-5de2-4aaf-8bf8-38ad27708603", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfb5700-3dea-4bb7-9275-fc992edce705", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d197bd35-6455-4a9c-a39b-400fa00848db", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/Variable Update Testing - Multiblock Example Python.ipynb b/examples/notebooks/Variable Update Testing - Multiblock Example Python.ipynb new file mode 100644 index 00000000..5b8081e8 --- /dev/null +++ b/examples/notebooks/Variable Update Testing - Multiblock Example Python.ipynb @@ -0,0 +1,310 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "1074e66d-b996-4bae-85d3-464550638651", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import numpy as np\n", + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e8b26455-710f-447d-b498-79b64e85b824", + "metadata": {}, + "outputs": [], + "source": [ + "from ansys.visor.viewer import Visor" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bcee0a20-9cb6-4424-b45c-11354e16a858", + "metadata": {}, + "outputs": [], + "source": [ + "############################\n", + "# Initialize and start Visor\n", + "############################" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bdafd5f6-67c1-4d73-bedc-1f87fde9ad58", + "metadata": {}, + "outputs": [], + "source": [ + "vis = Visor()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2f2d1dc5-d032-45ae-b257-a987b671b1cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8081\n" + ] + } + ], + "source": [ + "dataset_id1 = vis.start(\"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm\", metadata=\"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e973dbd6-66e0-4f31-af8d-227047c5e81d", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1000\"\n", + " height=\"500\"\n", + " src=\"http://localhost:8081\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x265eb075f10>" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IFrame(\"http://localhost:8081\", width=\"1000\", height=\"500\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8d661164-96a4-41de-8468-2cf4b5a6d802", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset ID: 7003740970679077\n" + ] + } + ], + "source": [ + "# Print dataset metadata and get the dataset ID\n", + "datasets = vis.list_datasets()\n", + "# print(json.dumps(datasets, indent=4))\n", + "# Print the dataset ID\n", + "print(f\"dataset ID: {dataset_id1}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "aef0fe4b-b016-4ca2-a87f-6ae446120f0f", + "metadata": {}, + "outputs": [], + "source": [ + "parts = vis.list_variables(dataset_id1)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "1e7d2d55-03db-4856-b950-9c80788c1d48", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'part_id': 1744850593192375,\n", + " 'part_name': 'level_1_block_0_part_0',\n", + " 'variables': [{'index': 0,\n", + " 'type': 'POINT',\n", + " 'name': 'Normals',\n", + " 'num_components': 3,\n", + " 'num_points': 962,\n", + " 'ranges': [(-0.9987165331840515, 0.9987165331840515),\n", + " (-0.9987165331840515, 0.9987165331840515),\n", + " (-1.0, 1.0)],\n", + " 'magnitude_range': (0.9999999680464912, 1.0000000418852286)},\n", + " {'index': 1,\n", + " 'type': 'POINT',\n", + " 'name': 'gradient_variable_0',\n", + " 'num_components': 1,\n", + " 'num_points': 962,\n", + " 'ranges': [(0.0, 1.0)],\n", + " 'magnitude_range': (0.0, 1.0)},\n", + " {'index': 2,\n", + " 'type': 'POINT',\n", + " 'name': 'constant_variable_1',\n", + " 'num_components': 1,\n", + " 'num_points': 962,\n", + " 'ranges': [(1.0, 1.0)],\n", + " 'magnitude_range': (1.0, 1.0)},\n", + " {'index': 3,\n", + " 'type': 'POINT',\n", + " 'name': 'random_variable_2',\n", + " 'num_components': 1,\n", + " 'num_points': 962,\n", + " 'ranges': [(0.001848379266448319, 0.9985101819038391)],\n", + " 'magnitude_range': (0.001848379266448319, 0.9985101819038391)}]}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the new variables for the test point dataArray (zeros):\n", + "part = parts[0]\n", + "part.to_dict()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7cfa7968-d2e7-4e97-81a2-cdb640ec35fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "level_1_block_0_part_0 has the following variables:\n", + " Normals::point::3\n", + " gradient_variable_0::point::1\n", + " constant_variable_1::point::1\n", + " random_variable_2::point::1\n", + "variable to update: Normals::point::3\n" + ] + } + ], + "source": [ + "variables = part.variables\n", + "var_names = [f\"{v.name}::{v.type.value}::{v.num_components}\" for v in variables]\n", + "var_to_update = variables[0]\n", + "\n", + "print(f\"{part.part_name} has the following variables:\")\n", + "print(' ' + '\\n '.join(var_names))\n", + "print(f\"variable to update: {var_to_update.name}::{var_to_update.type.value}::{var_to_update.num_components}\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "a1f0d446-11d0-4ca0-b759-b6614d51adb5", + "metadata": {}, + "outputs": [], + "source": [ + "# Create new vector data\n", + "new_vector_values = np.zeros((var_to_update.num_points, var_to_update.num_components))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "602431e0-1173-4676-a689-3f0d8e671ff3", + "metadata": {}, + "outputs": [], + "source": [ + "# Compile a dict with the vector variable metadtata + updated values\n", + "vector_update_info = {\n", + " \"type\": \"point\",\n", + " \"name\": var_to_update.name,\n", + " \"num_components\": var_to_update.num_components,\n", + " \"data\": new_vector_values,\n", + " \"part_id\": part.part_id\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4b1c0fc9-d7df-454c-884f-0566a35585a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the actual update command\n", + "vis.update_variables(dataset_id1, [vector_update_info])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df3ddc5d-0fa5-470e-a046-09f707cb1cc6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2015384d-5de2-4aaf-8bf8-38ad27708603", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfb5700-3dea-4bb7-9275-fc992edce705", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d197bd35-6455-4a9c-a39b-400fa00848db", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/Variable Update Testing - Plate Example HTTP API.ipynb b/examples/notebooks/Variable Update Testing - Plate Example HTTP API.ipynb new file mode 100644 index 00000000..fcdefc80 --- /dev/null +++ b/examples/notebooks/Variable Update Testing - Plate Example HTTP API.ipynb @@ -0,0 +1,293 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "1074e66d-b996-4bae-85d3-464550638651", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import numpy as np\n", + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e8b26455-710f-447d-b498-79b64e85b824", + "metadata": {}, + "outputs": [], + "source": [ + "import requests" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bcee0a20-9cb6-4424-b45c-11354e16a858", + "metadata": {}, + "outputs": [], + "source": [ + "# Start the Visor server on the command line using:\n", + "# visor-cli server start\n", + "\n", + "# Take note of the service endpoint and assign to the url variable here.\n", + "url = \"http://localhost:53211\"\n", + "\n", + "# Set up endpoints for the operations we will need\n", + "# Start\n", + "start_url = f\"{url}/start\"\n", + "# List datasets\n", + "list_datasets_url = f\"{url}/list_datasets\"\n", + "# List variables (for dataset)\n", + "def list_variables_url(dataset_id):\n", + " return f\"{url}/{dataset_id}/list_variables\"\n", + "# Update variables (for dataset)\n", + "def update_variables_url(dataset_id):\n", + " return f\"{url}/{dataset_id}/update_variables\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bdafd5f6-67c1-4d73-bedc-1f87fde9ad58", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the start url and payload\n", + "start_payload={\"file_path\": \"tests/files/plate.vtp\", \"metadata\": {\"name\":\"plate\", \"unit\":\"m\"}}\n", + "\n", + "# Submit the request\n", + "resp = requests.post(start_url, json=start_payload)\n", + "resp.content\n", + "dataset_id1 = resp.json().get(\"dataset_id\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e973dbd6-66e0-4f31-af8d-227047c5e81d", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1000\"\n", + " height=\"500\"\n", + " src=\"http://localhost:8081\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x28a35fe3cd0>" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IFrame(\"http://localhost:8081\", width=\"1000\", height=\"500\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8d661164-96a4-41de-8468-2cf4b5a6d802", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'5897798037468084': {'id': 5897798037468084,\n", + " 'name': 'plate',\n", + " 'unit': 'm',\n", + " 'file_path': 'tests/files/plate.vtp',\n", + " 'metadata_path': None}}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List datasets and get the dataset ID\n", + "resp = requests.get(list_datasets_url)\n", + "datasets = resp.json().get('datasets', {})\n", + "datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "aef0fe4b-b016-4ca2-a87f-6ae446120f0f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'part_id': 5897798037468084,\n", + " 'part_name': 'plate',\n", + " 'variables': [{'index': 0,\n", + " 'type': 'POINT',\n", + " 'name': 'Normal',\n", + " 'num_components': 3,\n", + " 'num_points': 24,\n", + " 'ranges': [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]],\n", + " 'magnitude_range': [0.0, 0.0]},\n", + " {'index': 1,\n", + " 'type': 'POINT',\n", + " 'name': 'UV',\n", + " 'num_components': 2,\n", + " 'num_points': 24,\n", + " 'ranges': [[-110.0, 110.0], [-20.0, 43.189998626708984]],\n", + " 'magnitude_range': [0.0, 118.17519190327182]},\n", + " {'index': 0,\n", + " 'type': 'CELL',\n", + " 'name': 'Colors',\n", + " 'num_components': 3,\n", + " 'num_points': 12,\n", + " 'ranges': [[255.0, 255.0], [255.0, 255.0], [255.0, 255.0]],\n", + " 'magnitude_range': [441.6729559300637, 441.6729559300637]}]}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List variables for dataset_id\n", + "resp = requests.get(list_variables_url(dataset_id1))\n", + "parts = resp.json().get('parts')\n", + "part_to_update = parts[0]\n", + "part_id = part_to_update.get(\"part_id\")\n", + "part_name = part_to_update.get(\"part_name\")\n", + "part_to_update" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "1e7d2d55-03db-4856-b950-9c80788c1d48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "updating plate, variable: Normal, POINT, 3\n" + ] + } + ], + "source": [ + "# Create the new variables for the test point dataArray (zeros):\n", + "variables = part_to_update.get(\"variables\")\n", + "\n", + "variable_to_update = variables[0]\n", + "name = variable_to_update[\"name\"]\n", + "var_type = variable_to_update[\"type\"]\n", + "num_points = variable_to_update[\"num_points\"]\n", + "num_components = variable_to_update[\"num_components\"]\n", + "\n", + "print(f'updating {part_name}, variable: {name}, {var_type}, {num_components}')\n", + "\n", + "new_vector_values = np.random.rand(num_points, num_components)\n", + "# need to flatten to a python list of num_points*num_components values\n", + "new_vector_values = new_vector_values.flatten().tolist()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "602431e0-1173-4676-a689-3f0d8e671ff3", + "metadata": {}, + "outputs": [], + "source": [ + "# Compile a dict with the vector variable metadtata + updated values\n", + "vector_update_info = {\n", + " \"type\": \"point\",\n", + " \"name\": name,\n", + " \"num_components\": num_components,\n", + " \"data\": new_vector_values,\n", + " \"part_id\": part_id\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "4b1c0fc9-d7df-454c-884f-0566a35585a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the actual update command\n", + "payload = {\"variables\": [vector_update_info]}\n", + "resp = requests.post(update_variables_url(dataset_id1), json=payload)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df3ddc5d-0fa5-470e-a046-09f707cb1cc6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2015384d-5de2-4aaf-8bf8-38ad27708603", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfb5700-3dea-4bb7-9275-fc992edce705", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d197bd35-6455-4a9c-a39b-400fa00848db", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/Variable Update Testing - Plate Example Python.ipynb b/examples/notebooks/Variable Update Testing - Plate Example Python.ipynb new file mode 100644 index 00000000..ec7250e0 --- /dev/null +++ b/examples/notebooks/Variable Update Testing - Plate Example Python.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "1074e66d-b996-4bae-85d3-464550638651", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import numpy as np\n", + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e8b26455-710f-447d-b498-79b64e85b824", + "metadata": {}, + "outputs": [], + "source": [ + "from ansys.visor.viewer import Metadata, Visor" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bcee0a20-9cb6-4424-b45c-11354e16a858", + "metadata": {}, + "outputs": [], + "source": [ + "############################\n", + "# Initialize and start Visor\n", + "############################" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bdafd5f6-67c1-4d73-bedc-1f87fde9ad58", + "metadata": {}, + "outputs": [], + "source": [ + "vis = Visor()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2f2d1dc5-d032-45ae-b257-a987b671b1cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8081\n" + ] + } + ], + "source": [ + "dataset_id1 = vis.start(\"../../tests/files/plate.vtp\", metadata=Metadata(name=\"sphere_1\", unit=\"m\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e973dbd6-66e0-4f31-af8d-227047c5e81d", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1000\"\n", + " height=\"500\"\n", + " src=\"http://localhost:8081\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x2c09ca40710>" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IFrame(\"http://localhost:8081\", width=\"1000\", height=\"500\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8d661164-96a4-41de-8468-2cf4b5a6d802", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset ID: 5547174014720414\n" + ] + } + ], + "source": [ + "# Print dataset metadata and get the dataset ID\n", + "datasets = vis.list_datasets()\n", + "# print(json.dumps(datasets, indent=4))\n", + "# Print the dataset ID\n", + "print(f\"dataset ID: {dataset_id1}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "aef0fe4b-b016-4ca2-a87f-6ae446120f0f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "VisorPartVariables(part_id=5547174014720414, part_name='sphere_1', variables=[VisorVariable(index=0, type=<VisorVtkVariableType.POINT: 'point'>, name='Normal', num_components=3, num_points=24, ranges=[(-1.0, 1.0), (-1.0, 1.0), (-1.0, 1.0)], magnitude_range=(1.0, 1.0)), VisorVariable(index=1, type=<VisorVtkVariableType.POINT: 'point'>, name='UV', num_components=2, num_points=24, ranges=[(-110.0, 110.0), (-20.0, 43.189998626708984)], magnitude_range=(0.0, 118.17519190327182)), VisorVariable(index=0, type=<VisorVtkVariableType.CELL: 'cell'>, name='Colors', num_components=3, num_points=12, ranges=[(255.0, 255.0), (255.0, 255.0), (255.0, 255.0)], magnitude_range=(441.6729559300637, 441.6729559300637))])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parts = vis.list_variables(dataset_id1)\n", + "part_to_update = parts[0]\n", + "part_to_update" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1e7d2d55-03db-4856-b950-9c80788c1d48", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the new variables for the test point dataArray (zeros):\n", + "variable_to_update = part_to_update.variables[0]\n", + "new_vector_values = np.random.rand(variable_to_update.num_points, variable_to_update.num_components)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "602431e0-1173-4676-a689-3f0d8e671ff3", + "metadata": {}, + "outputs": [], + "source": [ + "# Compile a dict with the vector variable metadtata + updated values\n", + "vector_update_info = {\n", + " \"type\": \"point\",\n", + " \"name\": variable_to_update.name,\n", + " \"num_components\": variable_to_update.num_components,\n", + " \"data\": new_vector_values\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "4b1c0fc9-d7df-454c-884f-0566a35585a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the actual update command\n", + "vis.update_variables(dataset_id1, [vector_update_info])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df3ddc5d-0fa5-470e-a046-09f707cb1cc6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2015384d-5de2-4aaf-8bf8-38ad27708603", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfb5700-3dea-4bb7-9275-fc992edce705", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d197bd35-6455-4a9c-a39b-400fa00848db", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/Variable Update Testing.ipynb b/examples/notebooks/Variable Update Testing.ipynb new file mode 100644 index 00000000..14f1abac --- /dev/null +++ b/examples/notebooks/Variable Update Testing.ipynb @@ -0,0 +1,377 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "1074e66d-b996-4bae-85d3-464550638651", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List\n", + "\n", + "import numpy as np\n", + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e8b26455-710f-447d-b498-79b64e85b824", + "metadata": {}, + "outputs": [], + "source": [ + "from vtk import vtkFloatArray, vtkSphereSource" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4f4feb1e-2022-4743-ae91-22bd9c8a4c04", + "metadata": {}, + "outputs": [], + "source": [ + "from ansys.visor.viewer import Metadata, Visor" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2df5e87e-ddc1-4a75-9045-0df11fea807a", + "metadata": {}, + "outputs": [], + "source": [ + "class MeshCreator:\n", + " def __init__(self, scale=1.0, xoffset=0.0):\n", + " self.scale = scale\n", + " self.xoffset = xoffset\n", + " self.polydata = self._generate_polydata()\n", + "\n", + " @property\n", + " def num_points(self):\n", + " return self.polydata.GetNumberOfPoints()\n", + " \n", + " def add_variable(self, name, num_components, values):\n", + " vals = np.asarray(values, dtype=np.float32)\n", + "\n", + " # Enforce correct shape\n", + " if vals.shape != (self.num_points, num_components):\n", + " raise ValueError(\n", + " f\"Expected shape ({self.num_points}, {num_components}), \"\n", + " f\"got {vals.shape}\"\n", + " )\n", + "\n", + " arr = vtkFloatArray()\n", + " arr.SetName(name)\n", + " arr.SetNumberOfComponents(num_components)\n", + " arr.SetNumberOfTuples(self.num_points)\n", + "\n", + " for i in range(self.num_points):\n", + " if num_components == 1:\n", + " arr.SetValue(i, float(vals[i, 0]))\n", + " else:\n", + " row = vals[i, :].tolist()\n", + " arr.SetTuple(i, row)\n", + " self.polydata.GetPointData().AddArray(arr)\n", + " self.polydata.GetPointData().SetActiveScalars(name)\n", + "\n", + " def add_constant_variable(self, name: str, num_components: int, constant_values: List[float]):\n", + " values = self.get_constant_values(num_components, constant_values)\n", + " self.add_variable(name, num_components, values)\n", + " \n", + " def add_random_variable(self, name: str, num_components: int, scale_factor=1.0):\n", + " values = self.get_random_values(num_components, scale_factor=scale_factor)\n", + " self.add_variable(name, num_components, values)\n", + " \n", + " def get_constant_values(self, num_components: int, constant_values: List[float]) -> np.ndarray:\n", + " \"\"\"\n", + " Create an array with a different constant value per component.\n", + " The values list is used to set the constant value across all points for the corresponding component.\n", + " \"\"\"\n", + " if len(constant_values) != num_components:\n", + " raise ValueError(\"constant_values list length needs to equal num_components\")\n", + "\n", + " # Numpy array of shape (self.num_points, num_components)\n", + " arr = np.empty((self.num_points, num_components), dtype=np.float32)\n", + " for i in range(0, num_components):\n", + " arr[:, i] = float(constant_values[i])\n", + " return arr\n", + "\n", + " def get_random_values(self, num_components=1, scale_factor=1.0) -> np.ndarray:\n", + " \"\"\"\n", + " Create an array with random variables for each component, from 0 to scale_factor.\n", + " \"\"\"\n", + " rng = np.random.default_rng()\n", + " return (scale_factor * rng.random((self.num_points, num_components), dtype=np.float32))\n", + "\n", + " def _generate_polydata(self):\n", + " source = vtkSphereSource()\n", + " source.SetRadius(self.scale)\n", + " source.SetThetaResolution(32)\n", + " source.SetPhiResolution(32)\n", + " source.SetCenter(self.xoffset, 0, 0)\n", + " source.Update()\n", + " return source.GetOutput()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "104d8815-65ec-47ec-9505-757e55dda739", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the names and number of components for the vector variable to be updated\n", + "TEST_VECTOR_NAME = \"test_vector\"\n", + "NUM_COMPONENTS = 3\n", + "\n", + "# Define the name of the scalar variable to be updated\n", + "TEST_SCALAR_NAME = \"test_scalar\"\n", + "\n", + "SCALE_FACTOR = 5.0" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "51e70f20-4301-4c20-a913-832bc347333b", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up first mesh\n", + "data_obj1 = MeshCreator(xoffset=-2)\n", + "\n", + "# Add the variables\n", + "data_obj1.add_random_variable(TEST_VECTOR_NAME, NUM_COMPONENTS, scale_factor=SCALE_FACTOR)\n", + "data_obj1.add_random_variable(TEST_SCALAR_NAME, 1, scale_factor=SCALE_FACTOR)\n", + "\n", + "# Retrieve the polydata object\n", + "polydata1 = data_obj1.polydata" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "eb6e65a0-7687-40f0-8787-4568cc7c7481", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up second mesh at xoffset=2\n", + "data_obj2 = MeshCreator(xoffset=2)\n", + "\n", + "# Add a single variable\n", + "data_obj2.add_random_variable(TEST_VECTOR_NAME, NUM_COMPONENTS, scale_factor=SCALE_FACTOR)\n", + "data_obj2.add_random_variable(TEST_SCALAR_NAME, 1, scale_factor=SCALE_FACTOR)\n", + "\n", + "# Retrieve the polydata object\n", + "polydata2 = data_obj2.polydata" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "bcee0a20-9cb6-4424-b45c-11354e16a858", + "metadata": {}, + "outputs": [], + "source": [ + "############################\n", + "# Initialize and start Visor\n", + "############################" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bdafd5f6-67c1-4d73-bedc-1f87fde9ad58", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8081\n" + ] + } + ], + "source": [ + "vis = Visor()\n", + "vis.start(polydata1, metadata=Metadata(name=\"sphere_1\", unit=\"m\"))" + ] + }, + { + "cell_type": "raw", + "id": "5b2b8027-289f-4f99-8c11-c018cbca8db9", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "da42a211-2fcc-44b0-972e-2d562c5c6628", + "metadata": {}, + "outputs": [], + "source": [ + "# Now add a new dataset which has the TEST_VARIABLE_NAME variable\n", + "vis.add_dataset(polydata2, metadata=Metadata(name=\"sphere_2\", unit=\"m\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e973dbd6-66e0-4f31-af8d-227047c5e81d", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1000\"\n", + " height=\"500\"\n", + " src=\"http://localhost:8081\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x150e8b87670>" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IFrame(\"http://localhost:8081\", width=\"1000\", height=\"500\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8d661164-96a4-41de-8468-2cf4b5a6d802", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dataset ID: 8825748995742563\n" + ] + } + ], + "source": [ + "# Print dataset metadata and get the dataset ID\n", + "datasets = vis.list_datasets()\n", + "# print(json.dumps(datasets, indent=4))\n", + "# Get the dataset ID\n", + "dataset_id = list(datasets.keys())[0]\n", + "print(f\"dataset ID: {dataset_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1e7d2d55-03db-4856-b950-9c80788c1d48", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the new variables for the test point dataArrays:\n", + "# a list of random values between 0 and 10\n", + "new_vector_values = data_obj1.get_constant_values(NUM_COMPONENTS, [1.0, 2.0, 3.5])\n", + "new_scalar_values = data_obj1.get_constant_values(1, [5.0])" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "602431e0-1173-4676-a689-3f0d8e671ff3", + "metadata": {}, + "outputs": [], + "source": [ + "# Compile a dict with the vector variable metadtata + updated values\n", + "vector_update_info = {\n", + " \"type\": \"point\",\n", + " \"name\": TEST_VECTOR_NAME,\n", + " \"num_components\": NUM_COMPONENTS,\n", + " \"data\": new_vector_values\n", + "}\n", + "# Compile a dict with the scalar variable metadtata + updated values\n", + "scalar_update_info = {\n", + " \"type\": \"point\",\n", + " \"name\": TEST_SCALAR_NAME,\n", + " \"num_components\": 1,\n", + " \"data\": new_scalar_values\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "4b1c0fc9-d7df-454c-884f-0566a35585a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the actual update command\n", + "vis.update_variables(dataset_id, [vector_update_info, scalar_update_info])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df3ddc5d-0fa5-470e-a046-09f707cb1cc6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2015384d-5de2-4aaf-8bf8-38ad27708603", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfb5700-3dea-4bb7-9275-fc992edce705", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d197bd35-6455-4a9c-a39b-400fa00848db", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/VisorAddRemoveDataset.ipynb b/examples/notebooks/VisorAddRemoveDataset.ipynb new file mode 100644 index 00000000..dd297663 --- /dev/null +++ b/examples/notebooks/VisorAddRemoveDataset.ipynb @@ -0,0 +1,143 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "56fc4ae0-abe2-49ec-88cd-868cca1cf7a1", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "\n", + "from ansys.visor.viewer import Visor" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52bf6de5-5a88-4205-a06f-628032786d7a", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize Visor on port 8083\n", + "visualizer = Visor(url='http://localhost:8083')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b23ca72-47ef-4dd9-945c-9097f6def6b2", + "metadata": {}, + "outputs": [], + "source": [ + "# Provide initial input via the `start` method. \n", + "# Note: the input and metadata are optional. \n", + "# If we just run start() with no args before adding datasets, it will render a blank scene.\n", + "dataset_id1 = visualizer.start('../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm', \"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afa579e6-f772-49ed-acf2-a99bb779d018", + "metadata": {}, + "outputs": [], + "source": [ + "# View the visualizer inline\n", + "IFrame(src='http://localhost:8083', width=1000, height=600)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a67a878-53ab-4b66-af1e-d179962549e8", + "metadata": {}, + "outputs": [], + "source": [ + "# Add a second dataset\n", + "dataset_id2 = visualizer.add_dataset('../../tests/files/plate.vtp')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a88b65e2-5d8e-43c6-adcc-ceacc3a843ad", + "metadata": {}, + "outputs": [], + "source": [ + "datasets = visualizer.list_datasets()\n", + "dataset_ids = list(datasets)\n", + "datasets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c724939f-d66a-4e39-aaab-d89513aee2a3", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.remove_dataset(dataset_id1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2dff0d8d-0c8e-46a1-8273-cef609a27a46", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.remove_dataset(dataset_id2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12a89f89-4c49-4c39-8bd7-9f7596bc5871", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.list_datasets()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ea84ee4-943e-403b-94f3-3533cf77f3bb", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "959f3911-16b4-4f6b-aa60-ddafae06c9ef", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/VisorSaveLoadState.ipynb b/examples/notebooks/VisorSaveLoadState.ipynb new file mode 100644 index 00000000..593d6209 --- /dev/null +++ b/examples/notebooks/VisorSaveLoadState.ipynb @@ -0,0 +1,238 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "56fc4ae0-abe2-49ec-88cd-868cca1cf7a1", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "\n", + "from ansys.visor.viewer import Visor" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "52bf6de5-5a88-4205-a06f-628032786d7a", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize Visor on port 8083\n", + "visualizer = Visor(url='http://localhost:8083')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b23ca72-47ef-4dd9-945c-9097f6def6b2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8083\n" + ] + } + ], + "source": [ + "# Provide initial input via the `start` method. \n", + "# Note: the input and metadata are optional. \n", + "# If we just run start() with no args before adding datasets, it will render a blank scene.\n", + "visualizer.start('../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm', \"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "afa579e6-f772-49ed-acf2-a99bb779d018", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1000\"\n", + " height=\"600\"\n", + " src=\"http://localhost:8083\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x1e0ebd94b20>" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# View the visualizer inline\n", + "IFrame(src='http://localhost:8083', width=1000, height=600)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e52bcb05-6c35-40af-9753-4160948024c0", + "metadata": {}, + "outputs": [], + "source": [ + "await visualizer.save_state(\"test1\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "327b9310-9d55-4c7d-abbf-2a48c128407a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'version': '1.0',\n", + " 'ui': {'darkTheme': False},\n", + " 'scene': {'unit': 'm',\n", + " 'dataset_states': {'test_object': {'parts': {'level_1_block_0_part_0': {'opacity': 0.33},\n", + " 'level_1_block_0_part_1': {'opacity': 0.66},\n", + " 'level_1_block_0_part_2': {'opacity': 0.99},\n", + " 'level_1_block_1_part_0': {'opacity': 0.33},\n", + " 'level_1_block_1_part_1': {'opacity': 0.66},\n", + " 'level_1_block_1_part_2': {'opacity': 0.99},\n", + " 'level_1_block_2_part_0': {'opacity': 0.82},\n", + " 'level_1_block_2_part_1': {'opacity': 0.82},\n", + " 'level_1_block_2_part_2': {'opacity': 0.82}}}}}}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "\n", + "with open(\"test1/visor.json\", 'r+') as f:\n", + " state = json.load(f)\n", + "state" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a67a878-53ab-4b66-af1e-d179962549e8", + "metadata": {}, + "outputs": [], + "source": [ + "# Add a second dataset\n", + "visualizer.add_dataset('../../tests/files/plate.vtp')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3a2a30b7-719e-4671-86a8-abd8fede866c", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.load_state(\"test1\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "293ed596-3f8f-4121-b818-8cb7fc880cdf", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "await visualizer.save_state(\"test1\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a88b65e2-5d8e-43c6-adcc-ceacc3a843ad", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'version': '1.0',\n", + " 'ui': {'darkTheme': False},\n", + " 'scene': {'unit': 'm',\n", + " 'dataset_states': {'test_object': {'parts': {'level_1_block_0_part_0': {'opacity': 0.33},\n", + " 'level_1_block_0_part_1': {'opacity': 0.66},\n", + " 'level_1_block_0_part_2': {'opacity': 0.99},\n", + " 'level_1_block_1_part_0': {'opacity': 0.33},\n", + " 'level_1_block_1_part_1': {'opacity': 0.66},\n", + " 'level_1_block_1_part_2': {'opacity': 0.99},\n", + " 'level_1_block_2_part_0': {'opacity': 0.04},\n", + " 'level_1_block_2_part_1': {'opacity': 0.04},\n", + " 'level_1_block_2_part_2': {'opacity': 0.82}}},\n", + " 'model': {'parts': {'model': {'opacity': 0.04}}}}}}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "\n", + "with open(\"test1/visor.json\", 'r+') as f:\n", + " state = json.load(f)\n", + "state" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "959f3911-16b4-4f6b-aa60-ddafae06c9ef", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.load_state(\"test1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "344e81a5-3e57-4f70-8b48-0d5ef2d2a435", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/VisorUsage.ipynb b/examples/notebooks/VisorUsage.ipynb new file mode 100644 index 00000000..a00bbad2 --- /dev/null +++ b/examples/notebooks/VisorUsage.ipynb @@ -0,0 +1,258 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "e3d36795-601a-4f30-b0e2-9b290c7c5ce4", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "\n", + "from ansys.visor.viewer import Visor\n", + "\n", + "asset_path1 = '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm'\n", + "asset_path2 = '../../examples/assets/tensors9.vtp'\n", + "\n", + "#######################################################\n", + "# Example running Visor in a jupyter notebook\n", + "# Shows how to create two separate Visor visualizers\n", + "# on two distinct ports (8081, 8082) and\n", + "# view them in an iframe\n", + "#######################################################" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7ee1d84c-cb55-45e8-934b-fc23b78ef406", + "metadata": {}, + "outputs": [], + "source": [ + "# Instantiate a Visor visualizer on port 8081\n", + "visor = Visor(url='http://localhost:8081')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8e5c72f6-0fed-46e7-8d1b-b628b01735f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8081\n" + ] + } + ], + "source": [ + "# Start the visualizer using the first asset\n", + "dataset_id1 = visor.start(asset_path1)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "86371399-3e20-4990-a6c1-fb210868717e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1200\"\n", + " height=\"600\"\n", + " src=\"http://localhost:8081\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x29a36babeb0>" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# View the visualizer inline\n", + "IFrame(src='http://localhost:8081', width=1200, height=600)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b877c05f-f2f5-4ac5-b5b3-fd2852a65830", + "metadata": {}, + "outputs": [], + "source": [ + "# Update the visualizer with the second asset\n", + "dataset_id2 = visor.update(asset_path2)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4d84dc32-29bb-4882-9683-5d234e585913", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'app_name': 'Visor Viewer',\n", + " 'host': 'localhost',\n", + " 'port': 8081,\n", + " 'standalone': True,\n", + " 'datasets': ['model'],\n", + " 'metadata': None}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Print the information about the first visualizer instance\n", + "visor.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "524d1ff2-7a21-45e1-94b1-b4f29822df7c", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize a second Visor visualizer on port 8082\n", + "visor1 = Visor(url='http://localhost:8082')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "dda46dc4-d2ff-4929-8efe-65fc43868a5d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8082\n" + ] + } + ], + "source": [ + "dataset_id3 = visor1.start(asset_path2)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "678a64ee-d78f-4c13-aed4-a73812220063", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1200\"\n", + " height=\"600\"\n", + " src=\"http://localhost:8082\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x29a727d4280>" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IFrame(src='http://localhost:8082', width=1200, height=600)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "012e7dda-0a8a-413d-ba67-580d3fea0191", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server stopped\n" + ] + } + ], + "source": [ + "visor.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "538734cf-ca11-4485-bf82-bd93d3d72a51", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server stopped\n" + ] + } + ], + "source": [ + "# Stop the second Visor instance\n", + "visor1.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02b1f605-a53a-496b-9840-5dc6fae54f96", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/VisorVtkInputExample.ipynb b/examples/notebooks/VisorVtkInputExample.ipynb new file mode 100644 index 00000000..4a2fbda7 --- /dev/null +++ b/examples/notebooks/VisorVtkInputExample.ipynb @@ -0,0 +1,188 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "248aeb44-9561-4884-945e-84e725a8ab7c", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "from vtk import vtkCubeSource\n", + "\n", + "from ansys.visor.viewer import Visor" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3902cded-c2a7-4831-823b-6286f55e6197", + "metadata": {}, + "outputs": [], + "source": [ + "def get_cube(x: float, y: float, z: float):\n", + " cube_source = vtkCubeSource()\n", + " cube_source.SetXLength(x)\n", + " cube_source.SetYLength(y)\n", + " cube_source.SetZLength(z)\n", + " cube_source.Update()\n", + " return cube_source.GetOutput()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "835c52e4-85bc-4586-ad84-01d335ac08eb", + "metadata": {}, + "outputs": [], + "source": [ + "cube = get_cube(1.0, 1.0, 1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "214ffa27-5eec-46bc-8364-df25d0436581", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer = Visor()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "87afc94c-c8f0-4a3e-bd5b-68fd9ba6e57e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8081\n" + ] + } + ], + "source": [ + "visualizer.start(input=cube)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "74920319-a446-4529-aa14-5fcf32538bd1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " <iframe\n", + " width=\"1200\"\n", + " height=\"600\"\n", + " src=\"http://localhost:8081\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " \n", + " ></iframe>\n", + " " + ], + "text/plain": [ + "<IPython.lib.display.IFrame at 0x1a78d4d63e0>" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IFrame(src='http://localhost:8081', width=1200, height=600)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "654ca2ee-2c76-4c34-a012-e1e8e914a209", + "metadata": {}, + "outputs": [], + "source": [ + "cube = get_cube(1.0, 1.0, 4.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1ff74f09-41d6-43ea-801b-2742c7cab928", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.update(cube)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "50be0f76-8a55-44dc-bedf-73ad9a57a6f1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server stopped\n" + ] + } + ], + "source": [ + "visualizer.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22ea54a3-2ae1-4b3f-b73c-7dd8e86d2635", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "480e91f2-c42a-4831-bfe8-0e529af85b12", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e8715ce-4e8a-4ea5-a95c-6be35fcc5607", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/python/asynchronous_start.py b/examples/python/asynchronous_start.py new file mode 100644 index 00000000..e2972637 --- /dev/null +++ b/examples/python/asynchronous_start.py @@ -0,0 +1,22 @@ +import asyncio +from os import path + +from ansys.visor.viewer import Visor +from ansys.visor.viewer.core.metadata import Metadata + + +async def start_update_stop(visualizer: Visor, input_file: str, update_file: str, metadata: Metadata): + await visualizer._start_async(input_file, metadata) + await asyncio.sleep(5) + visualizer.update(update_file, Metadata(name="test_model_2", unit="m")) + await asyncio.sleep(5) + await visualizer._stop_async() + +if __name__ == "__main__": + input_file = path.join(path.dirname(__file__), "../..", "tests", "files", "plate.vtp") + update_file = path.join(path.dirname(__file__), "../..", "tests", "files", "mesh.vtu") + metadata = Metadata(name="test_model", unit="m") + + visualizer = Visor(url="http://localhost:8081", standalone=True) + + asyncio.run(start_update_stop(visualizer, input_file, update_file, metadata)) diff --git a/examples/python/geometry_data_in_vtk_format.py b/examples/python/geometry_data_in_vtk_format.py new file mode 100644 index 00000000..3cf8ba02 --- /dev/null +++ b/examples/python/geometry_data_in_vtk_format.py @@ -0,0 +1,20 @@ +import pathlib +from os.path import join + +from ansys.visor.viewer import Visor +from ansys.visor.viewer.core.metadata import Metadata + + +def start_poc_session(): + abs_dir = pathlib.Path(__file__).parent.resolve() + assets_directory = join(abs_dir, "../assets") + file_path = join(assets_directory, "reactor.vtm") + visualizer: Visor = Visor(standalone=True) + visualizer.start(input=file_path, metadata= Metadata(name="reactor", unit = "m")) + +def main(): + start_poc_session() + + +if __name__ == "__main__": + main() diff --git a/examples/python/pick_geom_example.py b/examples/python/pick_geom_example.py new file mode 100644 index 00000000..425f23e7 --- /dev/null +++ b/examples/python/pick_geom_example.py @@ -0,0 +1,28 @@ +from ansys.visor.viewer import Visor +from ansys.visor.viewer.core.metadata import Metadata + + +def main(): + + # input_file = "tests/files/sample_hex_cube.vtp" + # input_file = "tests/files/cone_r0.5.vtp" + input_file = "tests/files/tria_cube.vtp" + print('Instantiating Visor') + # Initialize the Visor viewer + visor = Visor(url='http://0.0.0.0:8081', + standalone=True) + + # Start the viewer with the specified file path + print(f'Starting Visor viewer with input file: {input_file}') + visor.start( + input=input_file, + timeout=0, + metadata=Metadata(unit='m', name='Test VTM Scene'), + blocking=True + ) + +if __name__ == "__main__": + main() + + + diff --git a/examples/python/simple_example.py b/examples/python/simple_example.py new file mode 100644 index 00000000..4ba0aa16 --- /dev/null +++ b/examples/python/simple_example.py @@ -0,0 +1,26 @@ +from ansys.visor.viewer import Visor +from ansys.visor.viewer.core.metadata import Metadata + + +def main(): + + input_file = "tests/files/many_blocks/many_blocks.vtm" + print('Instantiating Visor') + # Initialize the Visor viewer + visor = Visor(url='http://0.0.0.0:8081', + standalone=True) + + # Start the viewer with the specified file path + print(f'Starting Visor viewer with input file: {input_file}') + visor.start( + input=input_file, + timeout=0, + metadata=Metadata(unit='m', name='Test VTM Scene'), + blocking=True + ) + +if __name__ == "__main__": + main() + + + diff --git a/examples/python/visor_dash_example.py b/examples/python/visor_dash_example.py new file mode 100644 index 00000000..3b3ebf7a --- /dev/null +++ b/examples/python/visor_dash_example.py @@ -0,0 +1,53 @@ +from dash import Dash, Input, Output, callback, html +from visordash import Visordash, init_endpoints + + +def get_app(): + app = Dash(__name__) + init_endpoints(app) + + td = Visordash( + id='input', + host='localhost', + port=8081, + aspectRatio=2.2, + pixelDensity=1500, + darkMode=False, + ) + + app.layout = html.Div([ + td, + html.Button("Get Parts Snapshot", id="snapshot-btn"), + html.Pre(id='output') + ]) + + @callback( + Output('input', 'fireSnapshot'), + Input('snapshot-btn', 'n_clicks'), + prevent_initial_call=True + ) + def trigger_snapshot(n_clicks): + return True + + @callback( + Output('output', 'children'), + Input('input', 'parts'), + prevent_initial_call=True + ) + def display_parts(parts): + import json + return json.dumps(parts, indent=2) + + return app + +def run_dash(): + app = get_app() + print("Dash started") + app.run(host='0.0.0.0', port=8050, debug=False, use_reloader=False) # use_reloader=False avoids double-execution + print("Dash exiting") + +if __name__ == '__main__': + try: + run_dash() + except KeyboardInterrupt: + print("\nKeyboardInterrupt received! Stopping...") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..7e0f6cfc --- /dev/null +++ b/poetry.lock @@ -0,0 +1,4847 @@ +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +description = "A collection of accessible pygments styles" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, + {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, +] + +[package.dependencies] +pygments = ">=1.5" + +[package.extras] +dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"] +tests = ["hypothesis", "pytest"] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.10" +groups = ["doc"] +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +description = "Document parameters, class attributes, return types, and variables inline, with Annotated." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101"}, + {file = "annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb"}, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, +] + +[[package]] +name = "ansys-sphinx-theme" +version = "1.9.0" +description = "A theme devised by ANSYS, Inc. for Sphinx documentation." +optional = false +python-versions = "<4,>=3.10" +groups = ["doc"] +files = [ + {file = "ansys_sphinx_theme-1.9.0-py3-none-any.whl", hash = "sha256:eb483928d86b1906aa4bfd9db225f1f67eb50ebbf7e9d7b9c652c87efe0ea038"}, + {file = "ansys_sphinx_theme-1.9.0.tar.gz", hash = "sha256:23a6d2f22f318a0ecc4954e8c017a5a6fccfcfa32d4e4446a9355f7516086cf9"}, +] + +[package.dependencies] +importlib-metadata = ">=4.0" +Jinja2 = ">=3.1.2" +pdf2image = ">=1.17.0" +pydata-sphinx-theme = ">=0.15.4,<0.19" +PyYAML = ">=6.0" +Sphinx = ">=6.1.0" + +[package.extras] +all = ["PyYAML (>=6.0.3)", "sphinx-autoapi (>=3.7.0)", "sphinx-design (>=0.6.0)", "sphinx-jinja (>=2.0.2)"] +autoapi = ["sphinx-autoapi (>=3.7.0)", "sphinx-design (>=0.6.0)", "sphinx-jinja (>=2.0.2)"] +changelog = ["PyYAML (>=6.0.3)", "sphinx-design (>=0.6.0)"] + +[[package]] +name = "anyio" +version = "4.14.2" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, +] + +[package.dependencies] +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "platform_system == \"Darwin\"" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "asttokens" +version = "3.0.2" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933"}, + {file = "asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2"}, +] + +[package.extras] +astroid = ["astroid (>=2,<5)"] +test = ["astroid (>=2,<5)", "pytest (<9.0)", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "babel" +version = "2.18.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, +] + +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["doc"] +files = [ + {file = "beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9"}, + {file = "beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7"}, +] + +[package.dependencies] +soupsieve = ">=1.6.1" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, +] + +[[package]] +name = "cffi" +version = "2.1.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "os_name == \"nt\" or implementation_name == \"pypy\"" +files = [ + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, + {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, + {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, +] + +[[package]] +name = "click" +version = "8.4.2" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\"", doc = "sys_platform == \"win32\"", setup = "platform_system == \"Windows\""} + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["setup"] +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +name = "comm" +version = "0.2.3" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "setup"] +files = [ + {file = "comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417"}, + {file = "comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971"}, +] + +[package.extras] +test = ["pytest"] + +[[package]] +name = "contourpy" +version = "1.3.3" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.11" +groups = ["main", "setup"] +files = [ + {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, + {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db"}, + {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620"}, + {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f"}, + {file = "contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff"}, + {file = "contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42"}, + {file = "contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470"}, + {file = "contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb"}, + {file = "contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1"}, + {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7"}, + {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411"}, + {file = "contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69"}, + {file = "contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b"}, + {file = "contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc"}, + {file = "contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5"}, + {file = "contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9"}, + {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659"}, + {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7"}, + {file = "contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d"}, + {file = "contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263"}, + {file = "contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9"}, + {file = "contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d"}, + {file = "contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b"}, + {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a"}, + {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e"}, + {file = "contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3"}, + {file = "contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8"}, + {file = "contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301"}, + {file = "contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a"}, + {file = "contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3"}, + {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b"}, + {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36"}, + {file = "contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d"}, + {file = "contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd"}, + {file = "contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339"}, + {file = "contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772"}, + {file = "contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0"}, + {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4"}, + {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f"}, + {file = "contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae"}, + {file = "contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc"}, + {file = "contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77"}, + {file = "contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880"}, +] + +[package.dependencies] +numpy = ">=1.25" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.17.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "coverage" +version = "7.15.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264"}, + {file = "coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04"}, + {file = "coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338"}, + {file = "coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7"}, + {file = "coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e"}, + {file = "coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc"}, + {file = "coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4"}, + {file = "coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b"}, + {file = "coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd"}, + {file = "coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff"}, + {file = "coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c"}, + {file = "coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4"}, + {file = "coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5"}, + {file = "coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7"}, + {file = "coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425"}, + {file = "coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839"}, + {file = "coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85"}, + {file = "coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e"}, + {file = "coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753"}, + {file = "coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2"}, + {file = "coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809"}, + {file = "coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc"}, + {file = "coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a"}, + {file = "coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b"}, + {file = "coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278"}, + {file = "coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84"}, + {file = "coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +groups = ["main", "setup"] +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "dash" +version = "4.4.1" +description = "A Python framework for building reactive web-apps. Developed by Plotly." +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "dash-4.4.1-py3-none-any.whl", hash = "sha256:72120a91b10ee4d73f9446efd5d6a4ec218086feed7b1b479d2259844d1f658f"}, + {file = "dash-4.4.1.tar.gz", hash = "sha256:9356ca7856bc496c12c5b6de5978aaf783e090d07450ee395ade1c0c2cbdf816"}, +] + +[package.dependencies] +coloredlogs = {version = ">=15.0.1", optional = true, markers = "extra == \"dev\""} +comm = "*" +fire = {version = ">=0.4.0", optional = true, markers = "extra == \"dev\""} +Flask = ">=1.0.4,<3.2" +importlib-metadata = "*" +janus = ">=1.0.0" +nest-asyncio = "*" +plotly = ">=5.0.0" +pydantic = ">=2.10" +PyYAML = {version = ">=5.4.1", optional = true, markers = "extra == \"dev\""} +requests = "*" +retrying = "*" +setuptools = "*" +typing_extensions = ">=4.1.1" +Werkzeug = "<3.2" + +[package.extras] +ag-grid = ["dash-ag-grid"] +async = ["flask[async]"] +celery = ["celery[redis] (>=5.1.2,<5.4.0)", "kombu (<5.4.0)", "redis (>=3.5.3,<=5.0.4)"] +ci = ["black (==22.3.0)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "ipython (<9.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "mypy (==1.15.0) ; python_version >= \"3.12\"", "numpy (<=1.26.3)", "openpyxl", "orjson (>=3.10.11)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pyright (==1.1.398) ; python_version >= \"3.7\"", "pytest-mock", "pytest-rerunfailures", "pytest-split", "pytest-sugar (==1.1.1)", "pyzmq (>=26.0.0)", "xlrd (>=2.0.1)"] +cloud = ["plotly-cloud"] +compress = ["flask-compress"] +dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] +diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] +fastapi = ["fastapi", "uvicorn[standard]"] +quart = ["quart"] +testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash_testing_stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy-python-selenium (>=1.0.0)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"] + +[[package]] +name = "debugpy" +version = "1.8.21" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "debugpy-1.8.21-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9"}, + {file = "debugpy-1.8.21-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344"}, + {file = "debugpy-1.8.21-cp310-cp310-win32.whl", hash = "sha256:72b5d676c4cbfac3bac5bb01c138a4656e843f93f03ce2a5f4e394ad49fbee73"}, + {file = "debugpy-1.8.21-cp310-cp310-win_amd64.whl", hash = "sha256:a7fe47fd23da57b9e0bec3f4a8ee65a2dc55782455ed7f2141d75ab5d2eaeef5"}, + {file = "debugpy-1.8.21-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264"}, + {file = "debugpy-1.8.21-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc"}, + {file = "debugpy-1.8.21-cp311-cp311-win32.whl", hash = "sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e"}, + {file = "debugpy-1.8.21-cp311-cp311-win_amd64.whl", hash = "sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7"}, + {file = "debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e"}, + {file = "debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176"}, + {file = "debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9"}, + {file = "debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c"}, + {file = "debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88"}, + {file = "debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2"}, + {file = "debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1"}, + {file = "debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0"}, + {file = "debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782"}, + {file = "debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e"}, + {file = "debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c"}, + {file = "debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8"}, + {file = "debugpy-1.8.21-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:0042da0ecd0a8b50dc4a54395ecd870d258d73fa18776f50c91fdcabdcad2675"}, + {file = "debugpy-1.8.21-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:ffd932c6796afadab6993ec96745918a8cb2444dbd392074f769db5ea40ab440"}, + {file = "debugpy-1.8.21-cp38-cp38-win32.whl", hash = "sha256:4e7c2d784d78ad4b71a5f8cd7b59c167719ec8a7a0211dbb3eb1bfeda78bc4e2"}, + {file = "debugpy-1.8.21-cp38-cp38-win_amd64.whl", hash = "sha256:aa9d941d6dfe3d0407e4b3ca0b9ec466030e260fbf1174094f68785680f66db6"}, + {file = "debugpy-1.8.21-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:9f5171176a0084b95d2ebe55a4d1f7b2a75b74c5dbec577ebd3a85c740551c36"}, + {file = "debugpy-1.8.21-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:f15c10084f9861b5e8414a48f18f8e4aadf51a98a59e72c16aa28281ca994672"}, + {file = "debugpy-1.8.21-cp39-cp39-win32.whl", hash = "sha256:4e70cc8b5079f885cb43910924ee0aab73b8b6b2a14eff23afdd9895d86e79eb"}, + {file = "debugpy-1.8.21-cp39-cp39-win_amd64.whl", hash = "sha256:e935f9dc0501be523c8a8e1853c39432e1354e9ece717ae5998fd2371c4542c3"}, + {file = "debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92"}, + {file = "debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6"}, +] + +[[package]] +name = "deepdiff" +version = "8.6.2" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "deepdiff-8.6.2-py3-none-any.whl", hash = "sha256:4d22034a866c3928303a9332c279362f714192d9305bac17c498720d095fd1b4"}, + {file = "deepdiff-8.6.2.tar.gz", hash = "sha256:186dcbd181e4d76cef11ab05f802d0056c5d6083c5a6748c1473e9d7481e183e"}, +] + +[package.dependencies] +orderly-set = ">=5.4.1,<6" + +[package.extras] +cli = ["click (>=8.1.0,<8.2.0)", "pyyaml (>=6.0.0,<6.1.0)"] +coverage = ["coverage (>=7.6.0,<7.7.0)"] +dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)", "jsonpickle (>=4.0.0,<4.1.0)", "nox (==2025.5.1)", "numpy (>=2.0,<3.0) ; python_version < \"3.10\"", "numpy (>=2.2.0,<2.3.0) ; python_version >= \"3.10\"", "orjson (>=3.10.0,<3.11.0)", "pandas (>=2.2.0,<2.3.0)", "polars (>=1.21.0,<1.22.0)", "python-dateutil (>=2.9.0,<2.10.0)", "tomli (>=2.2.0,<2.3.0)", "tomli-w (>=1.2.0,<1.3.0)", "uuid6 (==2025.0.1)"] +docs = ["Sphinx (>=6.2.0,<6.3.0)", "sphinx-sitemap (>=2.6.0,<2.7.0)", "sphinxemoji (>=0.3.0,<0.4.0)"] +optimize = ["orjson"] +static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"] +test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] + +[[package]] +name = "deepmerge" +version = "2.1.0" +description = "A toolset for deeply merging Python dictionaries." +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75"}, + {file = "deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pyupgrade", "sphinx", "sphinx-rtd-theme", "twine", "validate-pyproject[all]"] + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "executing" +version = "2.2.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, + {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] + +[[package]] +name = "fastapi" +version = "0.136.3" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620"}, + {file = "fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab"}, +] + +[package.dependencies] +annotated-doc = ">=0.0.2" +pydantic = ">=2.9.0" +starlette = ">=0.46.0" +typing-extensions = ">=4.8.0" +typing-inspection = ">=0.4.2" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fastjsonschema" +version = "2.22.1" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "fastjsonschema-2.22.1-py3-none-any.whl", hash = "sha256:cf377ff5c9a6f4f3125fb35f75a2c5767bd824ffbcf62c209a93cd48d1453999"}, + {file = "fastjsonschema-2.22.1.tar.gz", hash = "sha256:0b83d1ce8d7845b959dcb20e1a5c3c8883b6541d9c52ab02cce5166b75ec805f"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "fire" +version = "0.7.1" +description = "A library for automatically generating command line interfaces." +optional = false +python-versions = ">=3.7" +groups = ["setup"] +files = [ + {file = "fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882"}, + {file = "fire-0.7.1.tar.gz", hash = "sha256:3b208f05c736de98fb343310d090dcc4d8c78b2a89ea4f32b837c586270a9cbf"}, +] + +[package.dependencies] +termcolor = "*" + +[package.extras] +test = ["hypothesis (<6.136.0)", "levenshtein (<=0.27.1)", "pip", "pylint (<3.3.8)", "pytest (<=8.4.1)", "pytest-pylint (<=1.1.2)", "pytest-runner (<7.0.0)", "setuptools (<=80.9.0)", "termcolor (<3.2.0)"] + +[[package]] +name = "flask" +version = "3.1.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, + {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "fonttools" +version = "4.63.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b"}, + {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94"}, + {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579"}, + {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22"}, + {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e"}, + {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69"}, + {file = "fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e"}, + {file = "fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac"}, + {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f"}, + {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9"}, + {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b"}, + {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18"}, + {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0"}, + {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007"}, + {file = "fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb"}, + {file = "fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c"}, + {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02"}, + {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0"}, + {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af"}, + {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8"}, + {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b"}, + {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78"}, + {file = "fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263"}, + {file = "fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272"}, + {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd"}, + {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59"}, + {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d"}, + {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68"}, + {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be"}, + {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27"}, + {file = "fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380"}, + {file = "fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b"}, + {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745"}, + {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03"}, + {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49"}, + {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b"}, + {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6"}, + {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4"}, + {file = "fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616"}, + {file = "fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5"}, + {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001"}, + {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e"}, + {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096"}, + {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f"}, + {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40"}, + {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196"}, + {file = "fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8"}, + {file = "fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419"}, + {file = "fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d"}, + {file = "fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0"}, +] + +[package.extras] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.45.0)"] +symfont = ["sympy"] +type1 = ["xattr ; sys_platform == \"darwin\""] +unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + +[[package]] +name = "greenlet" +version = "3.5.5" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638"}, + {file = "greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0"}, + {file = "greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4"}, + {file = "greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc"}, + {file = "greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f"}, + {file = "greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db"}, + {file = "greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39"}, + {file = "greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53"}, + {file = "greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5"}, + {file = "greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759"}, + {file = "greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b"}, + {file = "greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2"}, + {file = "greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18"}, + {file = "greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52"}, + {file = "greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b"}, + {file = "greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537"}, + {file = "greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e"}, + {file = "greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd"}, + {file = "greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc"}, + {file = "greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53"}, + {file = "greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc"}, + {file = "greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9"}, + {file = "greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1"}, + {file = "greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07"}, + {file = "greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41"}, + {file = "greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874"}, + {file = "greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71"}, + {file = "greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0"}, + {file = "greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474"}, + {file = "greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007"}, + {file = "greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773"}, + {file = "greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e"}, + {file = "greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769"}, + {file = "greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f"}, + {file = "greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0"}, + {file = "greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5"}, + {file = "greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8"}, + {file = "greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b"}, + {file = "greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["setup"] +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + +[[package]] +name = "idna" +version = "3.18" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, +] + +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.5.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["doc"] +files = [ + {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, + {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.10" +groups = ["main", "doc", "setup"] +files = [ + {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, + {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "ipykernel" +version = "7.3.0" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057"}, + {file = "ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09"}, +] + +[package.dependencies] +appnope = {version = ">=0.1.2", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=8.9.0" +jupyter-core = ">=5.1,<6.0.dev0 || >=6.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio2 = ">=1.7.0" +packaging = ">=22" +psutil = ">=5.7" +pyzmq = ">=25" +tornado = ">=6.4.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "matplotlib", "pytest-cov", "trio"] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0,<10)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "9.16.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.11" +groups = ["dev"] +files = [ + {file = "ipython-9.16.1-py3-none-any.whl", hash = "sha256:4acae635506f6d352d94c4899a19d5f85f8bc4d230932342dca556fdab1c69b4"}, + {file = "ipython-9.16.1.tar.gz", hash = "sha256:5a3d1f9a47ff216d6cf9cf863124f6a2c1a198d1354c546a4d24a370a283b64c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.4", markers = "sys_platform == \"win32\""} +ipython-pygments-lexers = ">=1.0.0" +jedi = ">=0.18.2" +matplotlib-inline = ">=0.1.6" +pexpect = {version = ">4.6", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" +psutil = {version = ">=7", markers = "sys_platform != \"emscripten\" and sys_platform != \"cygwin\""} +pygments = ">=2.14.0" +stack_data = ">=0.6.0" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["argcomplete (>=3.0)", "ipython[doc,matplotlib,test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[matplotlib,test]", "setuptools (>=80.0)", "sphinx (>=8.0)", "sphinx-rtd-theme (>=0.1.8)", "sphinx_toml (==0.0.4)", "typing_extensions"] +matplotlib = ["matplotlib (>3.9)"] +test = ["packaging (>=23.0.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=1.0.0)", "setuptools (>=80.0)", "testpath (>=0.2)"] +test-extra = ["curio", "ipykernel (>6.30)", "ipython[matplotlib]", "ipython[test]", "jupyter_ai", "nbclient", "nbformat", "numpy (>=2.0)", "pandas (>2.1)", "trio (>=0.22.0)"] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +description = "Defines a variety of Pygments lexers for highlighting IPython code." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, + {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, +] + +[package.dependencies] +pygments = "*" + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main", "setup"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "janus" +version = "2.0.0" +description = "Mixed sync-async queue to interoperate between asyncio tasks and classic threads" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "janus-2.0.0-py3-none-any.whl", hash = "sha256:7e6449d34eab04cd016befbd7d8c0d8acaaaab67cb59e076a69149f9031745f9"}, + {file = "janus-2.0.0.tar.gz", hash = "sha256:0970f38e0e725400496c834a368a67ee551dc3b5ad0a257e132f5b46f2e77770"}, +] + +[[package]] +name = "jedi" +version = "0.20.0" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67"}, + {file = "jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011"}, +] + +[package.dependencies] +parso = ">=0.8.6,<0.9.0" + +[package.extras] +dev = ["Django", "attrs", "colorama", "docopt", "flake8 (==7.1.2)", "pytest (<9.0.0)", "types-setuptools (==80.9.0.20250529)", "typing-extensions", "zuban (==0.7.0)"] +docs = ["Jinja2 (==3.1.6)", "MarkupSafe (==3.0.3)", "Pygments (==2.20.0)", "Sphinx (==9.1.0)", "alabaster (==1.0.0)", "babel (==2.18.0)", "certifi (==2026.4.22)", "charset-normalizer (==3.4.7)", "docutils (==0.22.4)", "idna (==3.13)", "imagesize (==2.0.0)", "iniconfig (==2.3.0)", "packaging (==26.2)", "pluggy (==1.6.0)", "pytest (==9.0.3)", "requests (==2.33.1)", "roman-numerals (==4.1.0)", "snowballstemmer (==3.0.1)", "sphinx-rtd-theme (==3.1.0)", "sphinxcontrib-applehelp (==2.0.0)", "sphinxcontrib-devhelp (==2.0.0)", "sphinxcontrib-htmlhelp (==2.1.0)", "sphinxcontrib-jquery (==4.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==2.0.0)", "sphinxcontrib-serializinghtml (==2.0.0)", "urllib3 (==2.6.3)"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsonschema" +version = "4.26.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev", "doc"] +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.3.6" +referencing = ">=0.28.4" +rpds-py = ">=0.25.0" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["dev", "doc"] +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jupyter-client" +version = "8.9.1" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81"}, + {file = "jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa"}, +] + +[package.dependencies] +jupyter-core = ">=5.1" +python-dateutil = ">=2.8.2" +pyzmq = ">=25.0" +tornado = ">=6.4.1" +traitlets = ">=5.3" +typing-extensions = ">=4.13.0" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +orjson = ["orjson"] +test = ["anyio", "coverage", "ipykernel (>=6.14)", "msgpack", "mypy ; platform_python_implementation != \"PyPy\"", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.6.2)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407"}, + {file = "jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +traitlets = ">=5.3" + +[package.extras] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1"}, + {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "matplotlib" +version = "3.11.1" +description = "Python plotting package" +optional = false +python-versions = ">=3.11" +groups = ["main", "setup"] +files = [ + {file = "matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2"}, + {file = "matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c"}, + {file = "matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83"}, + {file = "matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099"}, + {file = "matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407"}, + {file = "matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f"}, + {file = "matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18"}, + {file = "matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74"}, + {file = "matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b"}, + {file = "matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea"}, + {file = "matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472"}, + {file = "matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481"}, + {file = "matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f"}, + {file = "matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a"}, + {file = "matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191"}, + {file = "matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1"}, + {file = "matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3"}, + {file = "matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e"}, + {file = "matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f"}, + {file = "matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b"}, + {file = "matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2"}, + {file = "matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f"}, + {file = "matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1"}, + {file = "matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464"}, + {file = "matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf"}, + {file = "matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f"}, + {file = "matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78"}, + {file = "matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a"}, + {file = "matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3"}, + {file = "matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319"}, + {file = "matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f"}, + {file = "matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be"}, + {file = "matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2"}, + {file = "matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6"}, + {file = "matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685"}, + {file = "matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae"}, + {file = "matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda"}, + {file = "matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb"}, + {file = "matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2"}, + {file = "matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d"}, + {file = "matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb"}, + {file = "matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987"}, + {file = "matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741"}, + {file = "matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb"}, + {file = "matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99"}, + {file = "matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.28.2" +kiwisolver = ">=1.3.1" +numpy = ">=1.25" +packaging = ">=20.0" +pillow = ">=9" +pyparsing = ">=3" +python-dateutil = ">=2.7" + +[[package]] +name = "matplotlib-inline" +version = "0.2.2" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6"}, + {file = "matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79"}, +] + +[package.dependencies] +traitlets = "*" + +[package.extras] +test = ["flake8", "matplotlib", "nbdime", "nbval", "notebook", "pytest"] + +[[package]] +name = "mistune" +version = "3.3.4" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a"}, + {file = "mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe"}, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192"}, + {file = "more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d"}, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, + {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, + {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, + {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, + {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, + {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, + {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, + {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, + {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, + {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, + {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, + {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, + {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, + {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, + {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, + {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, + {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, + {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, +] + +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] + +[[package]] +name = "narwhals" +version = "2.24.0" +description = "Extremely lightweight compatibility layer between dataframe libraries" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489"}, + {file = "narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d"}, +] + +[package.extras] +cudf = ["cudf-cu12 (>=24.10.0) ; sys_platform == \"linux\""] +dask = ["dask[dataframe] (>=2024.8)"] +duckdb = ["duckdb (>=1.1)"] +ibis = ["ibis-framework (>=6.0.0)", "packaging (>=21.3)", "pyarrow-hotfix (>=0.7)"] +modin = ["modin (>=0.22.0)"] +pandas = ["pandas (>=1.3.4)"] +polars = ["polars (>=0.20.4)"] +pyarrow = ["pyarrow (>=13.0.0)"] +pyspark = ["pyspark (>=3.5.0)"] +pyspark-connect = ["pyspark[connect] (>=3.5.0)"] +sql = ["narwhals[duckdb]", "sqlparse (>=0.5.5)"] +sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"] + +[[package]] +name = "nbformat" +version = "5.11.0" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "nbformat-5.11.0-py3-none-any.whl", hash = "sha256:f70a17f591a9ccd1c601d5e61a4b20972703926df0ba42458ce14bf575766bb6"}, + {file = "nbformat-5.11.0.tar.gz", hash = "sha256:7dbaed4a69cae28c2b4d44ab7430a6af4544fb89455023f6f21550be757b60c8"}, +] + +[package.dependencies] +fastjsonschema = ">=2.15" +jsonschema = ">=2.6" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +traitlets = ">=5.1" + +[package.extras] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["packaging", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nbval" +version = "0.11.0" +description = "A py.test plugin to validate Jupyter notebooks" +optional = false +python-versions = ">=3.7, <4" +groups = ["dev"] +files = [ + {file = "nbval-0.11.0-py2.py3-none-any.whl", hash = "sha256:307aecc866c9a1e8a13bb5bbb008a702bacfda2394dff6fe504a3108a58042a0"}, + {file = "nbval-0.11.0.tar.gz", hash = "sha256:77c95797607b0a968babd2597ee3494102d25c3ad37435debbdac0e46e379094"}, +] + +[package.dependencies] +coverage = "*" +ipykernel = "*" +jupyter-client = "*" +nbformat = "*" +pytest = ">=7" + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["main", "setup"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "nest-asyncio2" +version = "1.7.2" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01"}, + {file = "nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8"}, +] + +[[package]] +name = "numpy" +version = "2.4.6" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main", "setup"] +files = [ + {file = "numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538"}, + {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47"}, + {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93"}, + {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8"}, + {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6"}, + {file = "numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8"}, + {file = "numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147"}, + {file = "numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698"}, + {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f"}, + {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853"}, + {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a"}, + {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2"}, + {file = "numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45"}, + {file = "numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751"}, + {file = "numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3"}, + {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b"}, + {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089"}, + {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a"}, + {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605"}, + {file = "numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91"}, + {file = "numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359"}, + {file = "numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997"}, + {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20"}, + {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d"}, + {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67"}, + {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd"}, + {file = "numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab"}, + {file = "numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75"}, + {file = "numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096"}, + {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b"}, + {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8"}, + {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402"}, + {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb"}, + {file = "numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1"}, + {file = "numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261"}, + {file = "numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e"}, + {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43"}, + {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e"}, + {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895"}, + {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4"}, + {file = "numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063"}, + {file = "numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627"}, + {file = "numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73"}, + {file = "numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda"}, +] + +[[package]] +name = "numpydoc" +version = "1.8.0" +description = "Sphinx extension to support docstrings in Numpy format" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "numpydoc-1.8.0-py3-none-any.whl", hash = "sha256:72024c7fd5e17375dec3608a27c03303e8ad00c81292667955c6fea7a3ccf541"}, + {file = "numpydoc-1.8.0.tar.gz", hash = "sha256:022390ab7464a44f8737f79f8b31ce1d3cfa4b4af79ccaa1aac5e8368db587fb"}, +] + +[package.dependencies] +sphinx = ">=6" +tabulate = ">=0.8.10" + +[package.extras] +developer = ["pre-commit (>=3.3)", "tomli ; python_version < \"3.11\""] +doc = ["intersphinx-registry", "matplotlib (>=3.5)", "numpy (>=1.22)", "pydata-sphinx-theme (>=0.13.3)", "sphinx (>=7)"] +test = ["matplotlib", "pytest", "pytest-cov"] + +[[package]] +name = "orderly-set" +version = "5.5.0" +description = "Orderly set" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7"}, + {file = "orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce"}, +] + +[package.extras] +coverage = ["coverage (>=7.6.0,<7.7.0)"] +dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)"] +optimize = ["orjson"] +static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)"] +test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "packaging" +version = "26.3" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "parso" +version = "0.8.7" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c"}, + {file = "parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "types-setuptools (==67.2.0.1)", "zuban (==0.5.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pdf2image" +version = "1.17.0" +description = "A wrapper around the pdftoppm and pdftocairo command line tools to convert PDF to a PIL Image list." +optional = false +python-versions = "*" +groups = ["doc"] +files = [ + {file = "pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2"}, + {file = "pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57"}, +] + +[package.dependencies] +pillow = "*" + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "picobox" +version = "4.0.0" +description = "Dependency injection framework designed with Python in mind." +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "picobox-4.0.0-py3-none-any.whl", hash = "sha256:4c27eb689fe45dabd9e64c382e04418147d0b746d155b4e80057dbb7ff82027e"}, + {file = "picobox-4.0.0.tar.gz", hash = "sha256:114da1b5606b2f615e8b0eb68d04198ad9de75af5adbcf5b36fe4f664ab927b6"}, +] + +[[package]] +name = "pillow" +version = "12.3.0" +description = "Python Imaging Library (fork)" +optional = false +python-versions = ">=3.10" +groups = ["main", "doc", "setup"] +files = [ + {file = "pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a"}, + {file = "pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed"}, + {file = "pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1"}, + {file = "pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb"}, + {file = "pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5"}, + {file = "pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b"}, + {file = "pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a"}, + {file = "pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df"}, + {file = "pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f"}, + {file = "pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09"}, + {file = "pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e"}, + {file = "pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f"}, + {file = "pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8"}, + {file = "pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130"}, + {file = "pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a"}, + {file = "pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d"}, + {file = "pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931"}, + {file = "pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7"}, + {file = "pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c"}, + {file = "pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71"}, + {file = "pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827"}, + {file = "pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5"}, + {file = "pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9"}, + {file = "pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8"}, + {file = "pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418"}, + {file = "pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a"}, + {file = "pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "setuptools", "trove-classifiers (>=2024.10.12)"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.11.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.11.2-py3-none-any.whl", hash = "sha256:7f89089b6ea71bda7962953edcf784b2e2d9d285b40ad88be2bb75c6e9d82ab4"}, + {file = "platformdirs-4.11.2.tar.gz", hash = "sha256:3a2ae5fca3520a01ab1be8b45613537f52ddf5b5f6f53d88233892dfbf0cd82d"}, +] + +[[package]] +name = "playwright" +version = "1.62.0" +description = "A high-level API to automate web browsers" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "playwright-1.62.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f"}, + {file = "playwright-1.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034"}, + {file = "playwright-1.62.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da"}, + {file = "playwright-1.62.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3"}, + {file = "playwright-1.62.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff"}, + {file = "playwright-1.62.0-py3-none-win32.whl", hash = "sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c"}, + {file = "playwright-1.62.0-py3-none-win_amd64.whl", hash = "sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1"}, + {file = "playwright-1.62.0-py3-none-win_arm64.whl", hash = "sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1"}, +] + +[package.dependencies] +greenlet = ">=3.1.1,<4.0.0" +pyee = ">=13,<14" + +[[package]] +name = "plotly" +version = "6.9.0" +description = "An open-source interactive data visualization library for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "setup"] +files = [ + {file = "plotly-6.9.0-py3-none-any.whl", hash = "sha256:36bebe2f1bb13884774fe61689c329071446f6ce4a8927fb1f0d6fb24f581236"}, + {file = "plotly-6.9.0.tar.gz", hash = "sha256:967ad33e8c704fed051800d11d985eb206a9c795c14206b30a6f463ed9c67d0d"}, +] + +[package.dependencies] +narwhals = ">=1.15.1" +packaging = "*" + +[package.extras] +dev = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.3.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +dev-build = ["build", "jupyterlab", "pytest", "requests", "ruff (==0.11.12)"] +dev-core = ["pytest", "requests", "ruff (==0.11.12)"] +dev-optional = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.3.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +dev-pandas1 = ["numpy (>=1,<2)", "pandas (>=1,<2)", "setuptools (<82)"] +dev-pandas2 = ["pandas (>=2,<3)"] +dev-pandas3 = ["pandas (>=3) ; python_version >= \"3.11\""] +express = ["numpy (>=1.22)"] +kaleido = ["kaleido (>=1.3.0)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.53" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2"}, + {file = "prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6"}, +] + +[package.dependencies] +wcwidth = ">=0.1.4" + +[[package]] +name = "propcache" +version = "0.5.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, +] + +[[package]] +name = "psutil" +version = "7.2.2" +description = "Cross-platform lib for process and system monitoring." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, + {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, + {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, + {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, + {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, + {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, + {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, + {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, + {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] +test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "os_name == \"nt\" and implementation_name != \"PyPy\" or implementation_name == \"pypy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.46.4" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pydantic-settings" +version = "2.6.0" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"}, + {file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.18.0" +description = "Bootstrap-based Sphinx theme from the PyData community" +optional = false +python-versions = ">=3.10" +groups = ["doc"] +files = [ + {file = "pydata_sphinx_theme-0.18.0-py3-none-any.whl", hash = "sha256:fbe5401f26642d487e3c5b6dfcbf69b3b1d579e80dcc479a429632abe0a13929"}, + {file = "pydata_sphinx_theme-0.18.0.tar.gz", hash = "sha256:b4abc95ab02600872e060db07c79e056e87b7ea653ab1ffd0e0b1fa75a3003d4"}, +] + +[package.dependencies] +accessible-pygments = "*" +Babel = "*" +beautifulsoup4 = "*" +docutils = "!=0.17.0" +pygments = ">=2.7" +sphinx = ">=8.0,<10" +typing-extensions = "*" + +[package.extras] +a11y = ["pydata-sphinx-theme[test]"] +dev = ["pandoc", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml", "sphinx-theme-builder[cli]", "tox"] +doc = ["ablog (>=0.11.8)", "astroid (>=3,!=4.0.3)", "colorama", "graphviz", "ipykernel", "ipyleaflet", "ipywidgets", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (==3.6.1)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap (<2.7.0)", "sphinx-togglebutton", "sphinxcontrib-mermaid", "sphinxcontrib-youtube (>=1.4.1)", "sphinxext-rediraffe", "xarray"] +i18n = ["Babel", "jinja2"] +test = ["pytest", "pytest-cov", "pytest-playwright", "pytest-regressions", "sphinx[test]"] + +[[package]] +name = "pyee" +version = "13.0.1" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228"}, + {file = "pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "mypy", "pytest", "pytest-asyncio ; python_version >= \"3.4\"", "pytest-trio ; python_version >= \"3.7\"", "sphinx", "toml", "tox", "trio", "trio ; python_version > \"3.6\"", "trio-typing ; python_version > \"3.6\"", "twine", "twisted", "validate-pyproject[all]"] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev", "doc"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyparsing" +version = "3.3.2" +description = "pyparsing - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyreadline3" +version = "3.5.6" +description = "A python implementation of GNU readline." +optional = false +python-versions = ">=3.8" +groups = ["setup"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d"}, + {file = "pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, +] + +[package.dependencies] +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-base-url" +version = "2.1.0" +description = "pytest plugin for URL based testing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6"}, + {file = "pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" +requests = ">=2.9" + +[package.extras] +test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "pytest-localserver (>=0.7.1)", "tox (>=3.24.5)"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-html" +version = "4.2.0" +description = "pytest plugin for generating HTML reports" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_html-4.2.0-py3-none-any.whl", hash = "sha256:ff5caf3e17a974008e5816edda61168e6c3da442b078a44f8744865862a85636"}, + {file = "pytest_html-4.2.0.tar.gz", hash = "sha256:b6a88cba507500d8709959201e2e757d3941e859fd17cfd4ed87b16fc0c67912"}, +] + +[package.dependencies] +jinja2 = ">=3" +pytest = ">=7" +pytest-metadata = ">=2" + +[package.extras] +docs = ["pip-tools (>=6.13)"] +test = ["assertpy (>=1.1)", "beautifulsoup4 (>=4.11.1)", "black (>=22.1)", "flake8 (>=4.0.1)", "pre-commit (>=2.17)", "pytest-mock (>=3.7)", "pytest-rerunfailures (>=11.1.2)", "pytest-xdist (>=2.4)", "selenium (>=4.3)", "tox (>=3.24.5)"] + +[[package]] +name = "pytest-metadata" +version = "3.1.1" +description = "pytest plugin for test session metadata" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b"}, + {file = "pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "tox (>=3.24.5)"] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-playwright" +version = "0.7.2" +description = "A pytest wrapper with fixtures for Playwright to automate web browsers" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_playwright-0.7.2-py3-none-any.whl", hash = "sha256:8084e015b2b3ecff483c2160f1c8219b38b66c0d4578b23c0f700d1b0240ea38"}, + {file = "pytest_playwright-0.7.2.tar.gz", hash = "sha256:247b61123b28c7e8febb993a187a07e54f14a9aa04edc166f7a976d88f04c770"}, +] + +[package.dependencies] +playwright = ">=1.18" +pytest = ">=6.2.4,<10.0.0" +pytest-base-url = ">=1.0.0,<3.0.0" +python-slugify = ">=6.0.0,<9.0.0" + +[[package]] +name = "pytest-trio" +version = "0.8.0" +description = "Pytest plugin for trio" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-trio-0.8.0.tar.gz", hash = "sha256:8363db6336a79e6c53375a2123a41ddbeccc4aa93f93788651641789a56fb52e"}, + {file = "pytest_trio-0.8.0-py3-none-any.whl", hash = "sha256:e6a7e7351ae3e8ec3f4564d30ee77d1ec66e1df611226e5618dbb32f9545c841"}, +] + +[package.dependencies] +outcome = ">=1.1.0" +pytest = ">=7.2.0" +trio = ">=0.22.0" + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev", "setup"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.2.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "doc", "setup"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"}, + {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"}, + {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"}, + {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"}, + {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"}, + {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"}, + {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"}, + {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"}, + {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"}, + {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"}, + {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"}, + {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"}, + {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"}, + {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "referencing" +version = "0.37.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.10" +groups = ["dev", "doc"] +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "requests" +version = "2.34.2" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "retrying" +version = "1.4.2" +description = "Retrying" +optional = false +python-versions = ">=3.6" +groups = ["main", "setup"] +files = [ + {file = "retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59"}, + {file = "retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39"}, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +description = "Manipulate well-formed Roman numerals" +optional = false +python-versions = ">=3.10" +groups = ["doc"] +files = [ + {file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"}, + {file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"}, +] + +[[package]] +name = "roman-numerals-py" +version = "4.1.0" +description = "This package is deprecated, switch to roman-numerals." +optional = false +python-versions = ">=3.10" +groups = ["doc"] +files = [ + {file = "roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780"}, + {file = "roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9"}, +] + +[package.dependencies] +roman-numerals = "4.1.0" + +[[package]] +name = "rpds-py" +version = "2026.6.3" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.11" +groups = ["dev", "doc"] +files = [ + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"}, + {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"}, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +description = "Most extensible Python build backend with support for C/C++ extension modules" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"}, + {file = "setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev", "setup"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +description = "This package provides 36 stemmers for 34 languages generated from Snowball algorithms." +optional = false +python-versions = ">=3.3" +groups = ["doc"] +files = [ + {file = "snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752"}, + {file = "snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.9.2" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.10" +groups = ["doc"] +files = [ + {file = "soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823"}, + {file = "soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74"}, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +description = "Python documentation generator" +optional = false +python-versions = ">=3.11" +groups = ["doc"] +files = [ + {file = "sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3"}, + {file = "sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +roman-numerals-py = ">=1.0.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["betterproto (==2.0.0b6)", "mypy (==1.15.0)", "pypi-attestations (==0.0.21)", "pyright (==1.1.395)", "pytest (>=8.0)", "ruff (==0.9.9)", "sphinx-lint (>=0.9)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.19.0.20250219)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241128)", "types-requests (==2.32.0.20241016)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdist[psutil] (>=3.4)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "3.5.2" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +optional = false +python-versions = ">=3.11" +groups = ["doc"] +files = [ + {file = "sphinx_autodoc_typehints-3.5.2-py3-none-any.whl", hash = "sha256:0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c"}, + {file = "sphinx_autodoc_typehints-3.5.2.tar.gz", hash = "sha256:5fcd4a3eb7aa89424c1e2e32bedca66edc38367569c9169a80f4b3e934171fdb"}, +] + +[package.dependencies] +sphinx = ">=8.2.3" + +[package.extras] +docs = ["furo (>=2025.9.25)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.10.7)", "defusedxml (>=0.7.1)", "diff-cover (>=9.7.1)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "sphobjinv (>=2.3.1.3)", "typing-extensions (>=4.15)"] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +description = "Add a copy button to each of your code cells." +optional = false +python-versions = ">=3.7" +groups = ["doc"] +files = [ + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, +] + +[package.dependencies] +sphinx = ">=1.8" + +[package.extras] +code-style = ["pre-commit (==2.12.1)"] +rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] + +[[package]] +name = "sphinx-design" +version = "0.6.1" +description = "A sphinx extension for designing beautiful, view size responsive web components." +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c"}, + {file = "sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632"}, +] + +[package.dependencies] +sphinx = ">=6,<9" + +[package.extras] +code-style = ["pre-commit (>=3,<4)"] +rtd = ["myst-parser (>=2,<4)"] +testing = ["defusedxml", "myst-parser (>=2,<4)", "pytest (>=8.3,<9.0)", "pytest-cov", "pytest-regressions"] +testing-no-myst = ["defusedxml", "pytest (>=8.3,<9.0)", "pytest-cov", "pytest-regressions"] +theme-furo = ["furo (>=2024.7.18,<2024.8.0)"] +theme-im = ["sphinx-immaterial (>=0.12.2,<0.13.0)"] +theme-pydata = ["pydata-sphinx-theme (>=0.15.2,<0.16.0)"] +theme-rtd = ["sphinx-rtd-theme (>=2.0,<3.0)"] +theme-sbt = ["sphinx-book-theme (>=1.1,<2.0)"] + +[[package]] +name = "sphinx-gallery" +version = "0.18.0" +description = "A Sphinx extension that builds an HTML gallery of examples from any set of Python scripts." +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "sphinx_gallery-0.18.0-py3-none-any.whl", hash = "sha256:54317366e77b182672797e5b46ab13cca9a27eafc3142c59dc4c211d4afe3420"}, + {file = "sphinx_gallery-0.18.0.tar.gz", hash = "sha256:4b5b5bc305348c01d00cf66ad852cfd2dd8b67f7f32ae3e2820c01557b3f92f9"}, +] + +[package.dependencies] +pillow = "*" +sphinx = ">=5" + +[package.extras] +animations = ["sphinxcontrib-video"] +dev = ["absl-py", "graphviz", "intersphinx-registry", "ipython", "joblib", "jupyterlite-sphinx", "lxml", "matplotlib", "numpy", "packaging", "plotly", "pydata-sphinx-theme", "pytest", "pytest-coverage", "seaborn", "sphinxcontrib-video", "statsmodels"] +jupyterlite = ["jupyterlite-sphinx"] +parallel = ["joblib"] +recommender = ["numpy"] +show-api-usage = ["graphviz"] +show-memory = ["memory-profiler"] + +[[package]] +name = "sphinx-mdinclude" +version = "0.6.2" +description = "Markdown extension for Sphinx" +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "sphinx_mdinclude-0.6.2-py3-none-any.whl", hash = "sha256:648e78edb067c0e4bffc22943278d49d54a0714494743592032fa3ad82a86984"}, + {file = "sphinx_mdinclude-0.6.2.tar.gz", hash = "sha256:447462e82cb8be61404a2204227f920769eb923d2f57608e3325f3bb88286b4c"}, +] + +[package.dependencies] +docutils = ">=0.19,<1.0" +mistune = ">=3.0,<4.0" +pygments = ">=2.8" +sphinx = ">=6" + +[package.extras] +dev = ["attribution (==1.7.1)", "black (==24.4.2)", "coverage (==7.5.1)", "docutils (==0.20.1) ; python_version < \"3.9\"", "docutils (==0.21.2) ; python_version >= \"3.9\"", "flake8 (==7.0.0)", "flit (==3.9.0)", "mistune (==3.0.2)", "mypy (==1.10.0)", "sphinx (==7.1.2) ; python_version < \"3.9\"", "sphinx (==7.3.7) ; python_version >= \"3.9\"", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-httpdomain" +version = "2.0.0" +description = "Sphinx extension that provides a domain for documenting HTTP APIs." +optional = false +python-versions = ">=3.10" +groups = ["doc"] +files = [ + {file = "sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl", hash = "sha256:e968775c9994f8139cb6ff91e1f6a8557396a2cc08073997eed10d9b39f96df3"}, + {file = "sphinxcontrib_httpdomain-2.0.0.tar.gz", hash = "sha256:9e4e8733bf41ee4d9d5f9eb4dbf3cc2c22a665221ba42c5c3ae181b98af8855d"}, +] + +[package.dependencies] +sphinx = ">=6.0" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +groups = ["doc"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-openapi" +version = "0.8.4" +description = "OpenAPI (fka Swagger) spec renderer for Sphinx" +optional = false +python-versions = ">=3.7" +groups = ["doc"] +files = [ + {file = "sphinxcontrib-openapi-0.8.4.tar.gz", hash = "sha256:df883808a5b5e4b4113ad697185c43a3f42df3dce70453af78ba7076907e9a20"}, + {file = "sphinxcontrib_openapi-0.8.4-py3-none-any.whl", hash = "sha256:50911c18d452d9390ee3a384ef8dc8bde6135f542ba55691f81e1fbc0b71014e"}, +] + +[package.dependencies] +deepmerge = ">=0.1" +jsonschema = ">=2.5.1" +picobox = ">=2.2" +PyYAML = ">=3.12" +sphinx = ">=2.0" +sphinx-mdinclude = ">=0.5.2" +sphinxcontrib-httpdomain = ">=1.5.0" + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "starlette" +version = "1.6.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c"}, + {file = "starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "tabulate" +version = "0.10.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.10" +groups = ["doc"] +files = [ + {file = "tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3"}, + {file = "tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "termcolor" +version = "3.3.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.10" +groups = ["setup"] +files = [ + {file = "termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5"}, + {file = "termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + +[[package]] +name = "tornado" +version = "6.5.8" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403"}, + {file = "tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb"}, + {file = "tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58"}, + {file = "tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808"}, + {file = "tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22"}, + {file = "tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195"}, + {file = "tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836"}, + {file = "tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee"}, + {file = "tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26"}, + {file = "tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f"}, +] + +[[package]] +name = "traitlets" +version = "5.16.1" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b"}, + {file = "traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3) ; python_version < \"3.12\"", "argcomplete (>=3.5.2) ; python_version >= \"3.12\"", "mypy (>=2.0) ; implementation_name != \"pypy\"", "pre-commit", "pytest (>=7.0,<10.0)", "pytest-mock", "pytest-mypy-testing ; implementation_name != \"pypy\""] + +[[package]] +name = "trame" +version = "3.12.0" +description = "Trame, a framework to build applications in plain Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "trame-3.12.0-py3-none-any.whl", hash = "sha256:9b33020625e0d1710d060c0fabe7b3be0e31b5e5138439ec9a796faf6fe96915"}, + {file = "trame-3.12.0.tar.gz", hash = "sha256:88b861162cb8b025e84e93f17dcfd43a84d02d2c1608c9f6d58e3cd646a50c05"}, +] + +[package.dependencies] +pyyaml = "*" +trame-client = ">=3.10.1,<4" +trame-common = ">=1,<2" +trame-server = ">=3.4,<4" +wslink = ">=2.3.3" + +[package.extras] +app = ["pywebview"] +dev = ["pre-commit", "pytest", "ruff"] +jupyter = ["jupyterlab"] + +[[package]] +name = "trame-client" +version = "3.13.6" +description = "Internal client of trame" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "trame_client-3.13.6-py3-none-any.whl", hash = "sha256:b4acf325d037a3d118c00a89ebbb32418dea9064844cc23e45c9deddea0628a4"}, + {file = "trame_client-3.13.6.tar.gz", hash = "sha256:1ba7d1528ad082195c03b29c876db07817da741e2c9a50a32914534b24f31332"}, +] + +[package.dependencies] +trame-common = ">=0.2.0" + +[package.extras] +dev = ["pre-commit", "ruff"] +test = ["numpy", "pillow", "pixelmatch", "pytest", "pytest-asyncio", "pytest-playwright", "pytest-xprocess", "trame (>=3.13)"] + +[[package]] +name = "trame-common" +version = "1.2.7" +description = "Dependency less classes and functions for trame" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "trame_common-1.2.7-py3-none-any.whl", hash = "sha256:14d50c651505c41f9144261b67bc0c7b3524bf72893d56ad544e255cfa1c1ba1"}, + {file = "trame_common-1.2.7.tar.gz", hash = "sha256:bf31b565f78fb4628e5d2f4cca99041e34f0e31ebe44781c7d015589826e4e0d"}, +] + +[package.extras] +dev = ["nox", "pre-commit", "pytest (>=6)", "pytest-cov (>=3)", "ruff"] +test = ["nox", "pytest (>=6)", "pytest-asyncio", "pytest-cov (>=3)", "trame", "trame-server (>=3.13.0)"] + +[[package]] +name = "trame-server" +version = "3.14.0" +description = "Internal server side implementation of trame" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "trame_server-3.14.0-py3-none-any.whl", hash = "sha256:3519e99fdb962d0ecf29f0e8f969c84280ba0ef71d2cafe84a97e8b31d8052bd"}, + {file = "trame_server-3.14.0.tar.gz", hash = "sha256:ca4d8f6ba7e46b81bfe9e3f7eb83de3df9d35abb0f0921ba0cf00286a26756ea"}, +] + +[package.dependencies] +more-itertools = "*" +trame-common = ">=1.2.3" +wslink = ">=2.5.7,<3" + +[package.extras] +dev = ["nox", "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", "ruff", "trame"] + +[[package]] +name = "trame-vtklocal" +version = "0.16.4" +description = "VTK Local Rendering using WASM" +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "trame_vtklocal-0.16.4-py3-none-any.whl", hash = "sha256:21028d627a025694076e85bcb6cc564db6190652bfcdf04154c6158936e3601b"}, + {file = "trame_vtklocal-0.16.4.tar.gz", hash = "sha256:97c0d11e43913f79b9697fde65fad9ab752b92c2a28fcedd5344eca0b589b4e5"}, +] + +[package.dependencies] +packaging = "*" +trame = "*" +trame-common = "*" + +[package.extras] +dev = ["pre-commit", "pytest", "pytest-asyncio", "ruff", "trame-client[test]"] +vtk = ["vtk (>=9.4)"] + +[[package]] +name = "trio" +version = "0.34.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "trio-0.34.0-py3-none-any.whl", hash = "sha256:6c7c9f49917694dcdcd5f67abd168df5599eca480d61f29854d17a61a75c2f05"}, + {file = "trio-0.34.0.tar.gz", hash = "sha256:63b9485408bdfdde544fced107045a8c0086cdc4bd0ef2f797b9e0dd111b964b"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "typing-extensions" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.3" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd"}, + {file = "typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d"}, +] + +[package.dependencies] +typing-extensions = ">=4.15.0" + +[[package]] +name = "urllib3" +version = "2.7.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev", "doc", "setup"] +files = [ + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "uvicorn" +version = "0.24.0.post1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, + {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "vtk" +version = "9.6.1" +description = "VTK is an open-source toolkit for 3D computer graphics, image processing, and visualization" +optional = false +python-versions = "*" +groups = ["main", "setup"] +files = [ + {file = "vtk-9.6.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:43e9dbb046a18e81b0d434dfeccf58085e4249c281171bc4ee8c1aea687d6a1e"}, + {file = "vtk-9.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:feebb3e0ddb8d5fd6e42abddbf1f55e5b7a9e9d9c265175d81d44bdafa8a41c1"}, + {file = "vtk-9.6.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d88c70f82716a40a67d9d151c93414458da8ba376ad493a2d1d58ea2de8a74c7"}, + {file = "vtk-9.6.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:00120e937967c584ac6d5aae6c460b8e6a4df0d33ffcf64c0e7683feaf082a78"}, + {file = "vtk-9.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:bd999531a7007a24b5af8da2c763f4b7c6d6c235671832be2175b0a79db8b5cb"}, + {file = "vtk-9.6.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a2945c0320b5df8f697d49f7d759b2c230ac293188158574526c20bbcaf10241"}, + {file = "vtk-9.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b49b3c36e599f652077e60ead865957a65b557a1b53bcd60b26bdaabb81d170d"}, + {file = "vtk-9.6.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3b3537cae99226f3082d3aeef2350b7329ee3cef7e7bd88d4ecacfcbfdadfaeb"}, + {file = "vtk-9.6.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:30a21c6810d2465dc34dd5987f9fb566dcb8d4e65e06367d10a018c24eea6747"}, + {file = "vtk-9.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:dde3627b9d33b75efebe2465183cbc682a9f9a7c1529cf027a8871e60e11b3b2"}, + {file = "vtk-9.6.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:b8125e3e3bc3160e18853a15be98101d0efe662c16036179ab15ddf1669b32af"}, + {file = "vtk-9.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:956d05b8c53c6a9eba569de244e9c8229815bbb3e024bb9954fafe163407e66d"}, + {file = "vtk-9.6.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9728e8d41889a0f105b5d20a73a4da80f398b2cfe6057fa7a94cd61128c3ceb4"}, + {file = "vtk-9.6.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3b5ec2e56bd6165189aa2e6e896edda29460e63040f897e1a123a1592810266d"}, + {file = "vtk-9.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:4022fda8af46636f74c3c1932c2365da13a1dc8779a6b1ea4b13dc5bbcdb729f"}, + {file = "vtk-9.6.1-cp313-cp313-macosx_10_10_x86_64.whl", hash = "sha256:88983bce26f7665ac6e4fb7de16cf53b896140a1a6cadd942d3c13e7c74a8530"}, + {file = "vtk-9.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:94ed369a54c6cfacea0b34f42d7d3ef41fa06c1aabfc75d93cabdc9047454293"}, + {file = "vtk-9.6.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:deeb86794cd42f922ea75711b9717e45841777624203727eb84595b709af1382"}, + {file = "vtk-9.6.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fef8abc33168ad38b2622cf29048b7d5fe48a45789bf0a0421781f5cafa1e554"}, + {file = "vtk-9.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:a5db7b2ff8fc3f56b547c8b9b7bc117a869c902683c86ef5cd6197c087f66183"}, + {file = "vtk-9.6.1-cp314-cp314-macosx_10_10_x86_64.whl", hash = "sha256:91257894723dfced8be264915d81ba418d08e5bbdb4873da0f12b02c1e21f244"}, + {file = "vtk-9.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e119721774418fba34e95852efaefe5ac4156a4a270362fef06894cdc1377b6e"}, + {file = "vtk-9.6.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98df48bcd630a4ffa71ac09d6aebb69c628925902920419e3db838dc7f7ce0ed"}, + {file = "vtk-9.6.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3ea3c3e466a4a9cd8fa7e5b64595215467a7936c9f0fd61ec3275954c122a79c"}, + {file = "vtk-9.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:a05e12ab8c82e81b225feee5ca08fda5fff814520a11c2941cc866335d990e03"}, + {file = "vtk-9.6.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:043fb013a2669180180bd0ab667f318f2e1f14da69ae943192a7443f5ce3721a"}, + {file = "vtk-9.6.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d08b3760cbd8bffdbb4551033c4c9d87afe30e9e9d4c0e5cad29cbe7107c9af7"}, + {file = "vtk-9.6.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:db47e322cb59da35ba11c077090087fffd91de7f3ddb8a02bccbd68477c2b8c1"}, + {file = "vtk-9.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aee385583226f5ef31815da08e04b4dbeaa089ec75571a2e4d129fb378657915"}, + {file = "vtk-9.6.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:262bfa84abe90d9d6f2413cc914dc486a02518a02f6448af5fedd363312f3ae7"}, + {file = "vtk-9.6.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5ba0e6300ffbcda25397ffd80ebefcb5594f0d6bfa7e026f4367b1f87c9e2f50"}, + {file = "vtk-9.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:624aa9a095de10881f59f6bb63fa3662b1b20a76981d54937710df724ae0cc0f"}, +] + +[package.dependencies] +matplotlib = ">=2.0.0" + +[package.extras] +numpy = ["numpy (>=1.9)"] +web = ["wslink (>=1.0.4)"] + +[[package]] +name = "wcwidth" +version = "0.8.2" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85"}, + {file = "wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main", "setup"] +files = [ + {file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"}, + {file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"}, +] + +[package.dependencies] +markupsafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wslink" +version = "2.5.7" +description = "Python/JavaScript library for communicating over WebSocket" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "wslink-2.5.7-py3-none-any.whl", hash = "sha256:47d729f654d1ee0bb42b6f05b068c48d5fdacd3216e827c1e80ea09eca0fe287"}, + {file = "wslink-2.5.7.tar.gz", hash = "sha256:ed14c2b4749ea231a105a23334d2e0c7783204b2033f46104972a189ea235818"}, +] + +[package.dependencies] +aiohttp = "<4" +msgpack = ">=1,<2" + +[package.extras] +build = ["uv (>=0.10.7,<0.12.0)"] +ssl = ["cryptography"] + +[[package]] +name = "yarl" +version = "1.24.5" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["main", "setup"] +files = [ + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[[package]] +name = "zipp" +version = "4.1.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.10" +groups = ["main", "doc", "setup"] +files = [ + {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, + {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "a93d7da0ef2070cf3bc9f5a6ab98d6a176ef98d65af89d84e36449ac489b3245" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2c9cc03c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,172 @@ +[build-system] +requires = ["poetry-core>=1.0.0,<1.3.0", "setuptools>=65.0"] +build-backend = "poetry.core.masonry.api" + +# The following lines were added to enforce the poetry version needed for the project. +# It will be read by the setup_environment.py script to install this version. +[build-system-requirements] +build-system-version = "1.7.0" + +[tool.poetry] +name = "ansys-visor-viewer" +version = "1.0.0_beta" +description = "\"Visor 3D visualization web component framework\"" +authors = ["Visor Team <visor@ansys.com>"] +readme = "README.rst" +license = "Ansys License" +repository = "https://github.com/ansys/visor" +classifiers = [ + "Development Status :: Alpha", + "License :: Ansys License", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Multimedia :: Graphics :: 3D Rendering", + "Topic :: Multimedia :: Graphics :: Viewers", +] +packages = [ + { include = "ansys/visor/viewer", from = "src" }, + { include = "visordash", from = "src/ansys/visor/dash" }, + { include = "ansys/visor/viewer/cli", from = "src"} +] + +include = [ + { path = "src/ansys/visor/viewer/client_bundle/**", format = ["sdist", "wheel"] }, + { path = "src/ansys/visor/viewer/dash/visordash/**", format = ["sdist", "wheel"] }, + { path = "src/ansys/visor/viewer/dash/src/lib/**", format = ["sdist", "wheel"] }, + { path = "README.rst", format = ["sdist", "wheel"] }, + { path = "LICENSE", format = ["sdist", "wheel"] }, + { path = "VISOR_Legal_notice.pdf", format = ["sdist", "wheel"] }, +] + +[tool.poetry.scripts] +visor-setup = "ansys.visor.viewer.utils.autosetup:main" +visor-cli = "ansys.visor.viewer.cli.visor_cli:main" + +[tool.poetry.urls] +Bugs = "https://github.com/ansys/visor/issues" +Documentation = "https://github.com/pages/auth?nonce=d0e90489-388e-415e-a0ca-0dc8d2f3c1e3&page_id=60759470&path=L3ZlcnNpb24vZGV2L2luZGV4Lmh0bWw" +Source = "https://github.com/ansys/visor" +Discussions = "https://github.com/ansys/visor/discussions" +Releases = "https://github.com/ansys/visor/releases" +Changelog = "https://github.com/ansys/visor/blob/main/CHANGELOG.md" + +[tool.poetry.dependencies] +python = "^3.11" +trame = "3.12.0" +trame-server = "^3.6.0" +wslink = "^2.5.0" +trame-vtklocal = "0.16.4" +vtk = "9.6.1" +fastapi = "^0.136.3" +starlette = ">=1.3.1" +uvicorn = ">=0.24,<0.25" +pydantic_settings = "<=2.6.0" +requests = "^2.32.0" +dash = ">=2.16.1" +click = ">=8.3.3" +urllib3 = "^2.7.0" +aiohttp = ">=3.14.0" +pillow = ">=12.3.0" # CVE-2026-55380 + +[[tool.poetry.source]] +name = "vtk-wheels" +url = "https://wheels.vtk.org" +priority = "supplemental" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.3" +pytest-cov = "^3.0.0" +pytest-mock = "^3.14.0" +pytest-trio = "^0.8.0" +pytest-asyncio = "^1.2.0" +httpx = "^0.28.0" +pytest-playwright = "^0.7.2" +pytest-html = "^4.2.0" +nbval = "^0.11.0" +ipykernel = "^7.2.0" +jupyter-client = "^8.8.0" +nbformat = "^5.10.4" +deepdiff = "^8.6.1" + +[tool.poetry.group.setup.dependencies] +trame = "3.12.0" +trame-vtklocal = "0.16.4" +vtk = "9.6.1" +# Provides dash-generate-components CLI used by `npm run build:backends` +dash = {version = ">=2.0.0", extras = ["dev"]} + +[tool.poetry.group.doc] +optional = true + +[tool.poetry.group.doc.dependencies] +ansys-sphinx-theme = "^1.1.1" +numpydoc = "1.8.0" +sphinx = "^8.0.2" +sphinx-copybutton = "0.5.2" +sphinx-gallery = "^0.18.0" +sphinx-autodoc-typehints = "*" +sphinxcontrib-openapi = "^0.8.4" +sphinx-design = "^0.6.1" + +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "N"] +ignore = ["E501", "E402"] # E501: Line too long (formatter job), E402: Module imports (may be intentional) + +[tool.towncrier] +package = "ansys.viewer.visor" +directory = "doc/changelog.d" +filename = "doc/source/changelog.rst" +start_string = ".. towncrier release notes start\n" +template = "doc/changelog.d/changelog_template.jinja" +title_format = "`{version} <https://github.com/ansys/visor/releases/tag/v{version}>`_ - {project_date}" +issue_format = "`#{issue} <https://github.com/ansys/visor/pull/{issue}>`_" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true + +[[tool.towncrier.type]] +directory = "dependencies" +name = "Dependencies" +showcontent = true + +[[tool.towncrier.type]] +directory = "maintenance" +name = "Maintenance" +showcontent = true + +[[tool.towncrier.type]] +directory = "miscellaneous" +name = "Miscellaneous" +showcontent = true +[[tool.towncrier.type]] +directory = "test" +name = "Test" +showcontent = true + diff --git a/src/ansys/visor/dash/.babelrc b/src/ansys/visor/dash/.babelrc new file mode 100644 index 00000000..06a8ae6b --- /dev/null +++ b/src/ansys/visor/dash/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react"], + "env": { + "production": { + "plugins": ["@babel/plugin-proposal-object-rest-spread", "styled-jsx/babel"] + }, + "development": { + "plugins": ["@babel/plugin-proposal-object-rest-spread", "styled-jsx/babel"] + }, + "test": { + "plugins": ["@babel/plugin-proposal-object-rest-spread", "styled-jsx/babel-test"] + } + } +} diff --git a/src/ansys/visor/dash/.eslintrc.json b/src/ansys/visor/dash/.eslintrc.json new file mode 100644 index 00000000..ba984aa2 --- /dev/null +++ b/src/ansys/visor/dash/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "parser": "@babel/eslint-parser", + "parserOptions": { + "requireConfigFile": false, + "babelOptions": { + "presets": ["@babel/preset-react"] + } + }, + "plugins": ["react"], + "extends": ["eslint:recommended", "plugin:react/recommended"], + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": {} +} diff --git a/src/ansys/visor/dash/.prettierignore b/src/ansys/visor/dash/.prettierignore new file mode 100644 index 00000000..761f75a8 --- /dev/null +++ b/src/ansys/visor/dash/.prettierignore @@ -0,0 +1,6 @@ +dist/ +build/ +node_modules/ +*.min.js +*/deps/ +*/inst/deps/ diff --git a/src/ansys/visor/dash/.prettierrc b/src/ansys/visor/dash/.prettierrc new file mode 100644 index 00000000..1f4c4bbc --- /dev/null +++ b/src/ansys/visor/dash/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/src/ansys/visor/dash/package-lock.json b/src/ansys/visor/dash/package-lock.json new file mode 100644 index 00000000..77f1ff71 --- /dev/null +++ b/src/ansys/visor/dash/package-lock.json @@ -0,0 +1,9445 @@ +{ + "name": "visordash", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "visordash", + "version": "0.0.1", + "dependencies": { + "ajv": "^8.18.0", + "ramda": "^0.26.1" + }, + "devDependencies": { + "@babel/core": "^7.22.1", + "@babel/eslint-parser": "^7.22.0", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/preset-env": "^7.22.2", + "@babel/preset-react": "^7.22.3", + "@plotly/dash-component-plugins": "^1.2.3", + "@plotly/webpack-dash-dynamic-import": "^1.2.0", + "babel-loader": "^10.1.1", + "css-loader": "^6.8.1", + "eslint": "^8.0.0", + "eslint-plugin-react": "^7.0.0", + "prettier": "^3.0.0", + "prop-types": "^15.8.1", + "react": "^16.8.6", + "react-docgen": "^5.4.3", + "react-dom": "^16.8.6", + "style-loader": "^3.3.3", + "styled-jsx": "^3.2.1", + "webpack": "^5.84.1", + "webpack-cli": "^5.1.1", + "webpack-dev-server": "^4.15.0" + }, + "engines": { + "node": ">=8.11.0", + "npm": ">=6.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.29.7.tgz", + "integrity": "sha512-zxt+UJTOMKvUt3yOg+D58MLuz334pHp93qifMFcjIIO+9hN6t+ufw2gi7vDPMpxvfnHRR+3VVXvIjineCcgyXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", + "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz", + "integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", + "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.27.1", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@plotly/dash-component-plugins": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@plotly/dash-component-plugins/-/dash-component-plugins-1.2.3.tgz", + "integrity": "sha512-BjcGvqS+sbp15rnETIWyuPg8imvxboSrHyOlEBGBUurF5v1nHZpfbk4PAYpk8Q6K20ofScbNf1aXp/lOLHAemA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@plotly/webpack-dash-dynamic-import": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@plotly/webpack-dash-dynamic-import/-/webpack-dash-dynamic-import-1.3.0.tgz", + "integrity": "sha512-JuleFNu/DqpzjYABv54j2eJ9+bCUut2EjTrEbyPCvAnjdhabOLnJmGl3Tf6ChDadIfao2Q5yH/R1K2q6dElroQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", + "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.19.tgz", + "integrity": "sha512-3vMNr4TzNQyjHcRZadojpRaD9Ofr6LsonZAoQ+HMUa/9ORTPoxVIw0e0mpqWpdjj8xybyCM+oKOUH2vwFu/oEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-loader": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", + "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": "^18.20.0 || ^20.10.0 || >=22.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0 || ^8.0.0-beta.1", + "@rspack/core": "^1.0.0 || ^2.0.0-0", + "webpack": ">=5.61.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.4", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.4" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c8": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", + "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", + "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-to-babel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/estree-to-babel/-/estree-to-babel-3.2.1.tgz", + "integrity": "sha512-YNF+mZ/Wu2FU/gvmzuWtYc8rloubL7wfXCTgouFrnjGVXPA/EeYYA7pupXWrb3Iv1cTBeSSxxJIbK23l4MRNqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.1.6", + "@babel/types": "^7.2.0", + "c8": "^7.6.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.3.tgz", + "integrity": "sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/ramda": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-docgen": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", + "integrity": "sha512-xlLJyOlnfr8lLEEeaDZ+X2J/KJoe6Nr9AzxnkdQWush5hz2ZSu66w6iLMOScMmxoSHWpWMn+k3v5ZiyCfcWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.7.5", + "@babel/generator": "^7.12.11", + "@babel/runtime": "^7.7.6", + "ast-types": "^0.14.2", + "commander": "^2.19.0", + "doctrine": "^3.0.0", + "estree-to-babel": "^3.1.0", + "neo-async": "^2.6.1", + "node-dir": "^0.1.10", + "strip-indent": "^3.0.0" + }, + "bin": { + "react-docgen": "bin/react-docgen.js" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/spdy-transport/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.4.7.tgz", + "integrity": "sha512-PkImcCsovR39byv4Tz83tAPsYs2CiTPOmDSplhe0lsIFVYJyd7rzJ7fbm41vSNsF/lnO+Ob5n/jgMookwY0pww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "7.8.3", + "babel-plugin-syntax-jsx": "6.18.0", + "convert-source-map": "1.7.0", + "loader-utils": "1.2.3", + "source-map": "0.7.3", + "string-hash": "1.1.3", + "stylis": "3.5.4", + "stylis-rule-sheet": "0.0.10" + }, + "peerDependencies": { + "react": "15.x.x || 16.x.x || 17.x.x" + } + }, + "node_modules/styled-jsx/node_modules/@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/styled-jsx/node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/styled-jsx/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylis-rule-sheet": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "stylis": "^3.5.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.3.tgz", + "integrity": "sha512-adBYQLivcg1jbdKEJeqScJJFvgm4qY9+3tXw+jdG6lkVeqRJEtiQmSWjmth8GKmDZuX7sYM4YFxQsf0AzMfGGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.99.8", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", + "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/ansys/visor/dash/package.json b/src/ansys/visor/dash/package.json new file mode 100644 index 00000000..ea9fcc19 --- /dev/null +++ b/src/ansys/visor/dash/package.json @@ -0,0 +1,54 @@ +{ + "name": "visordash", + "version": "0.0.1", + "description": "Project Description", + "main": "build/index.js", + "scripts": { + "start": "webpack serve --config ./webpack.serve.config.js --open", + "build:js": "webpack --mode production", + "build:backends": "dash-generate-components ./src/lib/components visordash -p package-info.json --r-prefix '' --jl-prefix '' --ignore \\.test\\.", + "build:backends-activated": "(. venv/bin/activate || venv\\scripts\\activate && npm run build:backends)", + "build": "npm run build:js && npm run build:backends", + "build:activated": "npm run build:js && npm run build:backends-activated", + "lint": "eslint src/", + "format": "prettier --write src/", + "format:check": "prettier --check src/" + }, + "author": "Ben Brooks <ben.brooksiv@ansys.com>", + "license": "", + "dependencies": { + "ajv": "^8.18.0", + "ramda": "^0.26.1" + }, + "devDependencies": { + "@babel/core": "^7.22.1", + "@babel/eslint-parser": "^7.22.0", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/preset-env": "^7.22.2", + "@babel/preset-react": "^7.22.3", + "@plotly/dash-component-plugins": "^1.2.3", + "@plotly/webpack-dash-dynamic-import": "^1.2.0", + "babel-loader": "^10.1.1", + "css-loader": "^6.8.1", + "eslint": "^8.0.0", + "eslint-plugin-react": "^7.0.0", + "prettier": "^3.0.0", + "prop-types": "^15.8.1", + "react": "^16.8.6", + "react-docgen": "^5.4.3", + "react-dom": "^16.8.6", + "style-loader": "^3.3.3", + "styled-jsx": "^3.2.1", + "webpack": "^5.84.1", + "webpack-cli": "^5.1.1", + "webpack-dev-server": "^4.15.0" + }, + "overrides": { + "minimatch": "10.2.3", + "fast-uri": "^3.1.5" + }, + "engines": { + "node": ">=8.11.0", + "npm": ">=6.1.0" + } +} diff --git a/src/ansys/visor/dash/src/lib/components/Visordash.react.js b/src/ansys/visor/dash/src/lib/components/Visordash.react.js new file mode 100644 index 00000000..c9b0e073 --- /dev/null +++ b/src/ansys/visor/dash/src/lib/components/Visordash.react.js @@ -0,0 +1,156 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; + +/** + * @typedef VisorRemoteReactInterface + * @property {()=>void} attachOnSelectListener + */ + +const Visordash = (props) => { + const { id, host, port, aspectRatio, pixelDensity, darkMode, basePath, fireSnapshot, setProps } = + props; + + // Normalise basePath: strip trailing slash, default to empty string. + const bp = (basePath || '').replace(/\/$/, ''); + + // Store setProps in a ref to avoid stale closure + const setPropsRef = useRef(setProps); + useEffect(() => { + setPropsRef.current = setProps; + }, [setProps]); + + useEffect(() => { + // The "window.__visorArgs" object is + // passed to the main Visor React app. + const visorArgs = (window.__visorArgs ??= {}); + visorArgs.host = host; + visorArgs.port = port; + visorArgs.aspectRatio = aspectRatio; + visorArgs.pixelDensity = pixelDensity; + visorArgs.darkMode = darkMode; + visorArgs.basePath = bp; + loadAssetsAsync().then(); + + async function loadAssetsAsync() { + console.log(`loading assets...`); + const styleElem = document.createElement('link'); + const stylePromise = new Promise((resolve, reject) => { + styleElem.onload = () => { + resolve(); + }; + styleElem.onerror = () => reject(null); + styleElem.rel = 'stylesheet'; + styleElem.href = `${bp}/visordash/css.css?i=` + crypto.randomUUID(); + document.head.appendChild(styleElem); + }); + try { + await stylePromise; + } catch (err) { + if (err === null) { + // If error is null, then either the style or + // script failed to download. Therefore, retry. + styleElem.remove(); + console.log(`style failed to load. retrying...`); + // eslint-disable-next-line no-magic-numbers + setTimeout(loadAssetsAsync, 3000); + return; + } + throw err; + } + console.log(`style loaded. loading script...`); + /** @type{HTMLScriptElement}*/ + let script; + const scriptPromise = new Promise((resolve, reject) => { + script = document.createElement('script'); + script.src = `${bp}/visordash/js.js?i=` + crypto.randomUUID(); + script.type = 'module'; + script.async = false; + script.onload = () => { + document.head.appendChild(script); + resolve(); + }; + script.onerror = () => reject(null); + document.head.appendChild(script); + }); + await scriptPromise; + console.log('script loaded.'); + } + }, []); + + // Passive, on-demand snapshot + useEffect(() => { + if (fireSnapshot) { + (async () => { + const snapFn = window.__visorState?.getAppStateAsync; + if (typeof snapFn === 'function' && typeof setPropsRef.current === 'function') { + const appState = await snapFn(/*pass 'true' to key by name*/ true); + setPropsRef.current({ + parts: appState.toDict(), + fireSnapshot: false, + }); + } else if (typeof setPropsRef.current === 'function') { + setPropsRef.current({ parts: null, fireSnapshot: false }); + } + })(); + } + }, [fireSnapshot]); + + return ( + <div id={id} style={{ width: '100%', height: '100%' }}> + <div id="VisorContainer" style={{ width: '100%', height: '100%' }} /> + </div> + ); +}; + +Visordash.propTypes = { + /** + * The ID used to identify this component in Dash callbacks. + */ + id: PropTypes.string, + /** + * The host for the websocket connection + */ + host: PropTypes.string, + /** + * The port for the websocket connection + */ + port: PropTypes.number, + /** + * The aspect ratio of the viewer. + * Set to -1 to make the Visor component always fit its container. + */ + aspectRatio: PropTypes.number, + /** + * The pixel density of the viewer. + * Think of it like the "DPI" of the Visor component: the higher the pixel density, + * the smaller the UI panels and text. The lower the pixel density, the larger the UI panels and text. + * Default is 1,000. Set to -1 to disable. + */ + pixelDensity: PropTypes.number, + /** + * Enable/disable dark CSS theme. + */ + darkMode: PropTypes.bool, + /** + * URL prefix for all internal viewer assets (JS, CSS, WASM, fonts). + * Set this when deploying behind a reverse proxy with a path prefix, + * e.g. "/my-solution-prefix". Must start with "/" when non-empty and + * must NOT end with "/". Default is "" (no prefix). + */ + basePath: PropTypes.string, + /** + * When set to true, triggers the frontend to take a snapshot of the current parts state. + * The snapshot is returned via the `parts` prop and `fireSnapshot` is reset to false. + */ + fireSnapshot: PropTypes.bool, + /** + * Contains the latest snapshot of the parts state, returned from the frontend when `fireSnapshot` is triggered. + */ + parts: PropTypes.any, + /** + * Dash-injected function used to update component props from the frontend. + */ + setProps: PropTypes.func, +}; + +export default Visordash; diff --git a/src/ansys/visor/dash/src/lib/index.js b/src/ansys/visor/dash/src/lib/index.js new file mode 100644 index 00000000..5f260e37 --- /dev/null +++ b/src/ansys/visor/dash/src/lib/index.js @@ -0,0 +1,4 @@ +// eslint-disable import/prefer-default-export +import Visordash from './components/Visordash.react'; + +export { Visordash }; diff --git a/src/ansys/visor/dash/visordash/__init__.py b/src/ansys/visor/dash/visordash/__init__.py new file mode 100644 index 00000000..f8fcb3a9 --- /dev/null +++ b/src/ansys/visor/dash/visordash/__init__.py @@ -0,0 +1,332 @@ +# Copyright 2025 ANSYS, Inc. All Rights Reserved. +# Restricted Rights Legend: See LICENSE for details. +# ruff: noqa: F405 + +from __future__ import print_function as _ + +import json +import os +import sys + +import dash as _dash +from typing_extensions import Buffer + +# noinspection PyUnresolvedReferences +from ._imports_ import * # noqa: F403 +from ._imports_ import __all__ + +if not hasattr(_dash, '__plotly_dash') and not hasattr(_dash, 'development'): + print('Dash was not successfully imported. ' + 'Make sure you don\'t have a file ' + 'named \n"dash.py" in your current directory.', file=sys.stderr) + sys.exit(1) + +_basepath = os.path.dirname(__file__) +_filepath = os.path.abspath(os.path.join(_basepath, 'package-info.json')) +with open(_filepath) as f: + package = json.load(f) + +package_name = package['name'].replace(' ', '_').replace('-', '_') +__version__ = package['version'] + +_current_path = os.path.dirname(os.path.abspath(__file__)) + +_this_module = sys.modules[__name__] + +async_resources = [] + +_js_dist = [] + +_js_dist.extend( + [ + { + "relative_package_path": "async-{}.js".format(async_resource), + "external_url": ( + "https://unpkg.com/{0}@{2}" + "/{1}/async-{3}.js" + ).format(package_name, __name__, __version__, async_resource), + "namespace": package_name, + "async": True, + } + for async_resource in async_resources + ] +) + +# TODO: Figure out if unpkg link works +_js_dist.extend( + [ + { + "relative_package_path": "async-{}.js.map".format(async_resource), + "external_url": ( + "https://unpkg.com/{0}@{2}" + "/{1}/async-{3}.js.map" + ).format(package_name, __name__, __version__, async_resource), + "namespace": package_name, + "dynamic": True, + } + for async_resource in async_resources + ] +) + +_js_dist.extend( + [ + { + 'relative_package_path': 'visordash.min.js', + + 'namespace': package_name + }, + { + 'relative_package_path': 'visordash.min.js.map', + + 'namespace': package_name, + 'dynamic': True + } + ] +) + +_css_dist = [] + +for _component in __all__: + setattr(locals()[_component], '_js_dist', _js_dist) + setattr(locals()[_component], '_css_dist', _css_dist) + +######################################################################## +######################################################################## +######################################################################## + +import base64 +import gzip + +from dash import Dash +from flask import Response + +from .utils import rewrite_css_urls + +script_dir = os.path.dirname(os.path.abspath(__file__)) +if script_dir not in sys.path: + sys.path.append(script_dir) + +from __GITIGNORE_visor_css import getDarkThemeCss, getLightThemeCss +from __GITIGNORE_visor_font import getFontBase64 +from __GITIGNORE_visor_react_assets import getCss, getJs + + +def init_endpoints(dash_app, base_path: str = "", flask_prefix: str | None = None): + """ + Register Visordash asset endpoints on the given Dash (or DashProxy) application. + + Parameters + ---------- + dash_app : Dash or DashProxy + The Dash application instance. Any object that exposes a ``server`` + attribute pointing to a Flask application is accepted, which means + plain ``dash.Dash`` instances *and* ``dash_extensions`` ``DashProxy`` + instances both work. + base_path : str, optional + A URL prefix to prepend to every asset route, e.g. ``"/my-prefix"``. + Must start with ``"/"`` when non-empty and must **not** end with + ``"/"``. Default is ``""`` (no prefix). + flask_prefix : str or None, optional + Controls the prefix used when registering routes on the Flask + application. This is useful when the WSGI server or reverse proxy + strips a mount prefix from ``PATH_INFO`` (for example, Waitress + started with ``--url-prefix`` or a reverse proxy setting + ``SCRIPT_NAME``). If ``None`` (the default) the value of + ``base_path`` is used for Flask route registration, preserving + backwards compatibility. To register routes without any prefix + while still emitting asset URLs with ``base_path``, pass the + empty string ``""`` for ``flask_prefix``. + """ + if not isinstance(dash_app, Dash) and not hasattr(dash_app, 'server'): + raise Exception( + 'dash_app must be a Dash instance (or a DashProxy / any object ' + 'with a "server" Flask attribute)' + ) + if hasattr(Visordash, 'endpoints_set') and Visordash.endpoints_set: + raise Exception('Visordash endpoints have already been set') + Visordash.endpoints_set = True + + if base_path and not base_path.startswith("/"): + raise ValueError("base_path must start with '/' when non-empty") + + if flask_prefix is not None and flask_prefix and not flask_prefix.startswith("/"): + raise ValueError("flask_prefix must start with '/' when non-empty") + + # Normalise base_path: strip trailing slash, keep empty string as-is. + base_path = base_path.rstrip('/') if base_path else "" + + # Determine the prefix used for registering Flask routes. If not + # provided, default to base_path for backwards compatibility. + if flask_prefix is None: + flask_prefix = base_path + else: + # Keep explicit empty string as-is; otherwise normalise trailing slash + flask_prefix = flask_prefix.rstrip('/') if flask_prefix else "" + + _react_js: str | None = None + _react_css: str | None = None + _vtk_webassembly_mjs: str | None = None + _vtk_webassembly_mjs_async: str | None = None + _vtk_webassembly_wasm_bytes: bytes | None = None + _vtk_webassembly_wasm_async_bytes: bytes | None = None + _theme_dark_css: str | None = None + _theme_light_css: str | None = None + _font_bytes: bytes | None = None + + @dash_app.server.route(f'{flask_prefix}/visordash/js.js') + def react_js(): + nonlocal _react_js + if _react_js is None: + _react_js = getJs() + response = Response(_react_js, content_type='application/javascript') + response.headers.set('Cache-Control', 'max-age=0') + return response + + @dash_app.server.route(f'{flask_prefix}/visordash/css.css') + def react_css(): + nonlocal _react_css + if _react_css is None: + raw = getCss() + # Rewrite absolute asset paths embedded in the CSS bundle so that + # they respect the configured base_path prefix. Use a helper to + # keep this route handler concise and readable. + if base_path: + raw = rewrite_css_urls(raw, base_path) + _react_css = raw + response = Response(_react_css, content_type='text/css') + response.headers.set('Cache-Control', 'max-age=0') + return response + + @dash_app.server.route(f'{flask_prefix}/css/theme-dark.css') + def theme_dark(): + nonlocal _theme_dark_css + if _theme_dark_css is None: + _theme_dark_css = getDarkThemeCss() + response = Response(_theme_dark_css, content_type='text/css') + response.headers.set('Cache-Control', 'max-age=0') + return response + + @dash_app.server.route(f'{flask_prefix}/css/theme-light.css') + def theme_light(): + nonlocal _theme_light_css + if _theme_light_css is None: + _theme_light_css = getLightThemeCss() + response = Response(_theme_light_css, content_type='text/css') + response.headers.set('Cache-Control', 'max-age=0') + return response + + # @dash_app.server.route(f'{base_path}/wasm/vtkWebAssembly.mjs') + # def vtk_webassembly_mjs(): + # nonlocal _vtk_webassembly_mjs + # if _vtk_webassembly_mjs is None: + # _vtk_webassembly_mjs = getVtkMjs() + # response = Response(_vtk_webassembly_mjs, content_type='application/javascript') + # response.headers.set('Cache-Control', 'max-age=0') + # return response + + # @dash_app.server.route(f'{base_path}/wasm/vtkWebAssemblyAsync.mjs') + # def vtk_webassembly_mjs_async(): + # nonlocal _vtk_webassembly_mjs_async + # if _vtk_webassembly_mjs_async is None: + # _vtk_webassembly_mjs_async = getVtkAsyncMjs() + # response = Response(_vtk_webassembly_mjs_async, content_type='application/javascript') + # response.headers.set('Cache-Control', 'max-age=0') + # return response + + # @dash_app.server.route(f'{base_path}/wasm/vtkWebAssembly.wasm') + # def vtk_webassembly_wasm(): + # nonlocal _vtk_webassembly_wasm_bytes + # try: + # if _vtk_webassembly_wasm_bytes is None: + # zipped_base64_str = getVtkWasmBase64() + # zipped_bytes: Buffer | bytes = base64.b64decode(zipped_base64_str) + # _vtk_webassembly_wasm_bytes = gzip.decompress(zipped_bytes) + # except Exception as e: + # print(e) + # response = Response(_vtk_webassembly_wasm_bytes, content_type='application/wasm') + # response.headers.set('Cache-Control', 'max-age=0') + # return response + + # @dash_app.server.route(f'{base_path}/wasm/vtkWebAssemblyAsync.wasm') + # def vtk_webassembly_wasm_async(): + # nonlocal _vtk_webassembly_wasm_async_bytes + # try: + # if _vtk_webassembly_wasm_async_bytes is None: + # zipped_base64_str = getVtkAsyncWasmBase64() + # zipped_bytes: Buffer | bytes = base64.b64decode(zipped_base64_str) + # _vtk_webassembly_wasm_async_bytes = gzip.decompress(zipped_bytes) + # except Exception as e: + # print(e) + # response = Response(_vtk_webassembly_wasm_async_bytes, content_type='application/wasm') + # response.headers.set('Cache-Control', 'max-age=0') + # return response + + # @dash_app.server.route(f'{base_path}/wasm/<path:filename>') + # def wasm_assets(filename: str): + # nonlocal _vtk_webassembly_mjs, _vtk_webassembly_wasm_bytes + # nonlocal _vtk_webassembly_mjs_async, _vtk_webassembly_wasm_async_bytes + # # handle known names that are embedded via helper getters + # print(f"\n\n\nRequest for wasm asset: {filename}\n\n\n") + # if filename.endswith('.mjs'): + # # prefer getting bundled mjs content if available + # if filename in ('vtkWebAssembly.mjs', 'vtkWebAssemblyAsync.mjs'): + # if filename == 'vtkWebAssembly.mjs': + # content = getVtkMjs() + # _vtk_webassembly_mjs = content + # else: + # content = getVtkAsyncMjs() + # _vtk_webassembly_mjs_async = content + # response = Response(content, content_type='application/javascript') + # response.headers.set('Cache-Control', 'max-age=0') + # return response + # if filename.endswith('.wasm'): + # if filename in ('vtkWebAssembly.wasm', 'vtkWebAssemblyAsync.wasm'): + # # return decompressed wasm bytes from base64 helpers + # if filename == 'vtkWebAssembly.wasm': + # zipped_base64_str = getVtkWasmBase64() + # else: + # zipped_base64_str = getVtkAsyncWasmBase64() + # zipped_bytes: Buffer | bytes = base64.b64decode(zipped_base64_str) + # wasm_bytes = gzip.decompress(zipped_bytes) + # if filename == 'vtkWebAssembly.wasm': + # _vtk_webassembly_wasm_bytes = wasm_bytes + # else: + # _vtk_webassembly_wasm_async_bytes = wasm_bytes + # response = Response(wasm_bytes, content_type='application/wasm') + # response.headers.set('Cache-Control', 'max-age=0') + # return response + # # return Response(status=404) + + @dash_app.server.route(f'{flask_prefix}/fonts/source-sans-3.woff2') + def font(): + nonlocal _font_bytes + if _font_bytes is None: + zipped_base64_str = getFontBase64() + zipped_bytes: Buffer | bytes = base64.b64decode(zipped_base64_str) + _font_bytes = gzip.decompress(zipped_bytes) + response = Response(_font_bytes, content_type='application/font-woff') + response.headers.set('Cache-Control', 'max-age=0') + return response + + +if 1: + # Run this code in a guaranteed if-statement block so + # we don't pollute the outer scope with the functions + # defined here. This is just a 'hacky' way to write an + # immediately invoked function expression (IIFE) in Python. + def extra_init(): + endpoints_set = Visordash.endpoints_set if hasattr(Visordash, 'endpoints_set') else False + if not endpoints_set: + msg: str = 'init_endpoints(dash_app) must be called first, where' + raise Exception(f'{msg} dash_app is the current Dash application') + + + original_init = Visordash.__init__ + + + def new_init(self, **kwargs): + extra_init() + original_init(self, **kwargs) + + + Visordash.__init__ = new_init diff --git a/src/ansys/visor/dash/visordash/utils.py b/src/ansys/visor/dash/visordash/utils.py new file mode 100644 index 00000000..5539fe07 --- /dev/null +++ b/src/ansys/visor/dash/visordash/utils.py @@ -0,0 +1,47 @@ +"""Utility helpers for visordash. + +Currently contains CSS asset URL rewriting logic used by the package. +""" +import re + +# Precompile the CSS url(...) pattern for reuse. This matches url( +# optional-whitespace optional-quote /path ... optional-quote optional-whitespace ) +# and captures the optional quote and the path. The pattern avoids consuming +# trailing whitespace so callers may choose whether to preserve formatting. +_CSS_URL_PATTERN = re.compile( + r"url\(\s*(?P<quote>['\"]?)(?P<path>/[^\s'\")]+)(?P=quote)\s*\)", + re.IGNORECASE, +) + + +def rewrite_css_urls(raw: str, base_path: str) -> str: + """Rewrite absolute url(...) paths in the provided CSS bundle. + + Parameters + ---------- + raw: str + CSS content to rewrite + base_path: str + Prefix to prepend to any absolute path that starts with '/' + + The function rewrites only paths starting with a single leading slash + (e.g. /fonts/, /wasm/, /visordash/) by prepending ``base_path`` while + preserving quoting. It intentionally skips protocol-relative URLs + (//...), full URLs (http://, https://), and other scheme-like paths. + """ + if not base_path: + return raw + + def _rewrite(m: re.Match) -> str: + quote = m.group('quote') or '' + path = m.group('path') + # Skip protocol-relative URLs (//...) + if path.startswith('//'): + return m.group(0) + # Skip patterns like /http:... or /data:... which shouldn't be rewritten + if re.match(r"^/[a-zA-Z]+:", path): + return m.group(0) + return f"url({quote}{base_path}{path}{quote})" + + return _CSS_URL_PATTERN.sub(_rewrite, raw) + diff --git a/src/ansys/visor/dash/webpack.config.js b/src/ansys/visor/dash/webpack.config.js new file mode 100644 index 00000000..1a5b4bc5 --- /dev/null +++ b/src/ansys/visor/dash/webpack.config.js @@ -0,0 +1,113 @@ +const path = require('path'); +const webpack = require('webpack'); +const WebpackDashDynamicImport = require('@plotly/webpack-dash-dynamic-import'); +const packagejson = require('./package.json'); + +const dashLibraryName = packagejson.name.replace(/-/g, '_'); + +module.exports = (env, argv) => { + + let mode; + + const overrides = module.exports || {}; + + // if user specified mode flag take that value + if (argv && argv.mode) { + mode = argv.mode; + } + + // else if configuration object is already set (module.exports) use that value + else if (overrides.mode) { + mode = overrides.mode; + } + + // else take webpack default (production) + else { + mode = 'production'; + } + + let filename = (overrides.output || {}).filename; + if(!filename) { + const modeSuffix = mode === 'development' ? 'dev' : 'min'; + filename = `${dashLibraryName}.${modeSuffix}.js`; + } + + const entry = overrides.entry || {main: './src/lib/index.js'}; + + const devtool = overrides.devtool || 'source-map'; + + const externals = ('externals' in overrides) ? overrides.externals : ({ + react: 'React', + 'react-dom': 'ReactDOM', + 'plotly.js': 'Plotly', + 'prop-types': 'PropTypes', + }); + + return { + mode, + entry, + output: { + path: path.resolve(__dirname, dashLibraryName), + chunkFilename: '[name].js', + filename, + library: dashLibraryName, + libraryTarget: 'window', + }, + devtool, + devServer: { + static: { + directory: path.join(__dirname, '/') + } + }, + externals, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + }, + }, + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + }, + ], + }, + ], + }, + optimization: { + splitChunks: { + name: '[name].js', + cacheGroups: { + async: { + chunks: 'async', + minSize: 0, + name(module, chunks, cacheGroupKey) { + return `${cacheGroupKey}-${chunks[0].name}`; + } + }, + shared: { + chunks: 'all', + minSize: 0, + minChunks: 2, + name: 'visordash-shared' + } + } + } + }, + plugins: [ + new WebpackDashDynamicImport(), + new webpack.SourceMapDevToolPlugin({ + filename: '[file].map', + exclude: ['async-plotlyjs'] + }) + ] + } +}; diff --git a/src/ansys/visor/dash/webpack.serve.config.js b/src/ansys/visor/dash/webpack.serve.config.js new file mode 100644 index 00000000..09a1a7ae --- /dev/null +++ b/src/ansys/visor/dash/webpack.serve.config.js @@ -0,0 +1,12 @@ +const config = require('./webpack.config.js'); +const path = require('path'); + +config.entry = {main: './src/demo/index.js'}; +config.output = { + filename: './output.js', + path: path.resolve(__dirname), +}; +config.mode = 'development'; +config.externals = undefined; // eslint-disable-line +config.devtool = 'inline-source-map'; +module.exports = config; diff --git a/src/ansys/visor/viewer/__init__.py b/src/ansys/visor/viewer/__init__.py new file mode 100644 index 00000000..c7ed50e8 --- /dev/null +++ b/src/ansys/visor/viewer/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2025 ANSYS, Inc. All Rights Reserved. +# Restricted Rights Legend: See LICENSE for details. + +from ansys.visor.viewer.app.visor import Visor +from ansys.visor.viewer.core.metadata import Metadata + +__all__ = ['Visor', 'Metadata'] + +# Version +# ------------------------------------------------------------------------------ + +try: + import importlib.metadata as importlib_metadata # type: ignore +except ModuleNotFoundError: # pragma: no cover + import importlib_metadata # type: ignore +__version__ = importlib_metadata.version("ansys-visor-viewer") + +VERSION = __version__ diff --git a/src/ansys/visor/viewer/api/__init__.py b/src/ansys/visor/viewer/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/api/models.py b/src/ansys/visor/viewer/api/models.py new file mode 100644 index 00000000..24b48247 --- /dev/null +++ b/src/ansys/visor/viewer/api/models.py @@ -0,0 +1,100 @@ +"""Models for the Visor Viewer API.""" + +from pydantic import BaseModel, Field, field_validator + +from ansys.visor.viewer.core.metadata import Metadata +from ansys.visor.viewer.core.visor_enums import RenderingMode +from ansys.visor.viewer.core.visor_helpers import validate_host + + +class Info(BaseModel): + """Input for getting the current state of the visualizer instance.""" + app_name: str = Field(..., description="Application name") + host: str = Field(..., description="Host address") + port: int = Field(..., description="Port number") + standalone: bool = Field(..., description="Standalone mode") + datasets: list[str] = Field(description="Path to the input file") + +class InitProps(BaseModel): + """Properties for initializing the server.""" + host: str = Field(..., description="Host address", examples=["localhost"]) + port: int = Field( + 0, + description=( + "Port number. Use 0 (the default) to let the Visor server pick an " + "unused port on its own host." + ), + examples=[0], + ) + standalone: bool | None = Field(None, description="Standalone", examples=[True]) + dark_mode: bool | None = Field(None, description="Dark mode enabled", examples=[False]) + rendering_mode: RenderingMode | None = Field(None, description="Rendering mode (LOCAL)", examples=["LOCAL"]) + + @field_validator("rendering_mode", mode="before") + @classmethod + def coerce_rendering_mode(cls, v): + """Accept both the enum member and its name as a string (e.g. 'LOCAL', 'REMOTE').""" + if v is None or isinstance(v, RenderingMode): + return v + if isinstance(v, str): + try: + return RenderingMode[v.upper()] + except KeyError: + valid = [e.name for e in RenderingMode] + raise ValueError(f"Invalid rendering mode {v!r}. Valid values: {valid}") + return v + + @field_validator("host") + @classmethod + def check_host(cls, v): + """Validate the host address.""" + if not validate_host(v): + raise ValueError("Invalid host address") + return v + +class StartProps(BaseModel): + """Properties for starting the visualizer instance.""" + file_path: str | None = Field(None, description="Path to the input file", examples=["path/to/file.vtk"]) + metadata: Metadata | str | None = Field(None, description="Metadata for the visualizer", examples=[{"name": "test_model", "unit": "m"}]) + timeout: int | None = Field(0, description="Timeout in seconds") + +class UpdateProps(BaseModel): + """Input for updating the visualizer instance.""" + file_path: str = Field(..., description="Path to the new input file", examples=["path/to/updated_file.vtk"]) + metadata: Metadata | str | None = Field(None, description="Metadata for the visualizer", examples=[{"name": "updated_model", "unit": "m"}]) + +class RemoveDatasetProps(BaseModel): + """Input for removing a dataset from the visualizer instance.""" + dataset_id: int = Field(..., description="ID of the dataset to remove", examples=[12345]) + +class UpdateVariableInfo(BaseModel): + """Input for updating a variable in the visualizer instance.""" + name: str = Field(..., description="Name of the variable to update", examples=["temperature"]) + type: str = Field(..., description="Type of the variable (point/cell)", examples=["point"]) + num_components: int = Field(..., description="Number of components", examples=[1]) + data: list[float] = Field(..., description="Data array for the variable", examples=[[0.0, 1.0, 2.0, 3.0]]) + part_id: int | None = Field( + None, + description=( + "Part ID targeting a specific leaf block in a multiblock dataset. " + "Obtain valid part_id values from the list_variables endpoint. " + "For composite (multiblock) datasets: if set, the update is applied only to that part. " + "If omitted (null/None), the update is broadcast to every part where the variable " + "exists and the data length matches โ€” parts that do not satisfy both conditions are " + "skipped with a warning. " + "For single-block datasets this field is ignored." + ), + examples=[1234567890123456], + ) + +class UpdateVariableProps(BaseModel): + """Input for updating a variable in the visualizer instance.""" + variables: list[UpdateVariableInfo] = Field(..., description="List of variables to update") + +class SaveStateProps(BaseModel): + """Input for saving the current state of the visualizer instance.""" + state_dir: str = Field(..., description="Path to directory to save the state", examples=["path/to/state_dir"]) + +class LoadStateProps(BaseModel): + """Input for loading a saved state into the visualizer instance.""" + state_dir: str = Field(..., description="Path to directory to load the state from", examples=["path/to/state_dir"]) diff --git a/src/ansys/visor/viewer/api/server.py b/src/ansys/visor/viewer/api/server.py new file mode 100644 index 00000000..fc859ea6 --- /dev/null +++ b/src/ansys/visor/viewer/api/server.py @@ -0,0 +1,433 @@ +"""FastAPI-based service for managing Visor visualizer instances""" + +import functools +from typing import Tuple + +from fastapi import ( + Body, + FastAPI, + HTTPException, +) + +import ansys.visor.viewer.core.errors as errors +from ansys.visor.viewer import Visor +from ansys.visor.viewer.api.models import ( + Info, + InitProps, + LoadStateProps, + RemoveDatasetProps, + SaveStateProps, + StartProps, + UpdateProps, + UpdateVariableProps, +) +from ansys.visor.viewer.api.visor_cache import VisorCache +from ansys.visor.viewer.config import settings +from ansys.visor.viewer.core.net import find_unused_port +from ansys.visor.viewer.core.visor_enums import RenderingMode +from ansys.visor.viewer.core.visor_logging import VisorLogger + +logger = VisorLogger(__name__, "server.log") + + + +def handle_exceptions(func): + """ + Decorator for async route handlers to catch known errors, + log them, and return appropriate HTTP responses. + + Args: + func: The async function to wrap. + + Returns: + The wrapped async function with error handling. + """ + @functools.wraps(func) + async def wrapper(self, *args, **kwargs): + """ Wrapper function to handle exceptions for async route handlers.""" + try: + return await func(self, *args, **kwargs) + except errors.VisorAddDatasetError as ade: + # For dataset-id returning APIs, we want to surface the error and + # return a sentinel dataset_id so clients don't mistake it for a valid ID. + msg = f"Failed to add dataset: {ade}" + logger.error(msg) + return {"success": False, "dataset_id": -1, "error": msg} + except errors.InvalidUrlError as ue: + msg = f"Invalid URL: {ue}" + logger.error(msg) + raise HTTPException(status_code=422, detail=msg) + except errors.InvalidFileError as f: + msg = f"Invalid file: {f}" + logger.error(msg) + raise HTTPException(status_code=422, detail=msg) + except errors.InvalidFileFormatError as ff: + msg = f"Invalid file format: {ff}" + logger.error(msg) + raise HTTPException(status_code=422, detail=msg) + except errors.ServerNotStartedError as se: + msg = f"Server not started: {se}" + logger.error(msg) + raise HTTPException(status_code=400, detail=msg) + except TypeError as te: + msg = f"Type error: {te}" + logger.error(msg) + raise HTTPException(status_code=500, detail=msg) + except RuntimeError as re: + msg = f"Runtime error: {re}" + logger.error(msg) + raise HTTPException(status_code=500, detail=msg) + except HTTPException: + raise + except Exception as e: + msg = f"Unhandled exception: {e}" + logger.error(msg) + raise HTTPException(status_code=500, detail=msg) + return wrapper + + +class VisorAPI: + """ + FastAPI-based API for managing a Visor visualizer instance. + + Provides endpoints to initialize, start, update, stop, + check the health of the visualizer, and retrieve server info + + Public route methods: + info() -> Info + Get the information of the visualizer instance. + connect_or_initialize_server(init_props: InitProps) + Connects to an existing instance on the provided host and port, + if it exists. + If a Visor instance has not already been initialized + on the provided host and port, it creates it with the provided + standalone and dark_mode properties. + list_instances() + Get the URL of the visualizer. + start_instance(start_props: StartProps) + Start the visualizer instance. + update(update_props: UpdateProps) + Update the input file of the visualizer instance. + stop_visualization() + Stop the visualization but keep the Visor instance. + stop_instance() + Stop the visualization and delete the Visor instance from + the cache. + health() + Get the health status of the visualizer instance. + + Protected helper methods: + _get_url(host: str, port: int) -> str + Get the URL for a host and port. + _initialize_visualizer(host: str, port: int) -> Visor + Initialize a Visor instance with the given host and port. + _stop_visualization() -> str + Stop the visualizer instance and return the URL. + """ + def __init__(self): + """Initialize the VisorAPI instance.""" + self.settings = settings + + # Initialize Visor instance with default settings + self.standalone = self.settings.default_standalone + self.trame_log_dir = self.settings.trame_log_dir + self.visualizer: Visor = self._connect_or_initialize_visualizer( + self.settings.default_host, + self.settings.default_port, + self.settings.default_standalone, + self.settings.default_dark_mode, + )[0] + + # Configure FastAPI application + self.app = FastAPI() + + # Register routes + self.app.get("/info")(self.info) + self.app.post("/initialize")(self.connect_or_initialize_server) + self.app.get("/")(self.list_instances) + self.app.post("/start")(self.start_instance) + self.app.post("/update")(self.update) + self.app.post("/stop_visualization")(self.stop_visualization) + self.app.post("/stop")(self.stop_instance) + self.app.post("/add_dataset")(self.add_dataset) + self.app.post("/remove_dataset")(self.remove_dataset) + self.app.get("/list_datasets")(self.list_datasets) + self.app.get("/{dataset_id}/list_variables")(self.list_variables) + self.app.post("/{dataset_id}/update_variables")(self.update_variables) + self.app.post("/save_state")(self.save_state) + self.app.post("/load_state")(self.load_state) + self.app.get("/health")(self.health_live) + + # Route method definitions + async def info(self) -> Info: + '''Get the information of the visualizer instance''' + if self.visualizer is not None: + info_dict = self.visualizer.info() + else: + info_dict = { + "app_name": self.settings.app_name, + "host": self.settings.default_host, + "port": self.settings.default_port, + "standalone": self.settings.default_standalone, + "datasets": [] + } + return Info(**info_dict) + + @handle_exceptions + async def connect_or_initialize_server(self, init_props: InitProps = Body( + examples=[{ + "host": "localhost", + "port": 0, + "standalone": False, + "dark_mode": False + }] + )): + """ + If an existing visualizer instance exists on the provided host and port, connect + to that instance, making it the active instance. + If no such instance exists, initialize a new visualizer instance on the provided + host and port. + """ + host = init_props.host + port = init_props.port + if port == 0: + # Visor owns port selection: probe on the exact interface the + # visualizer will actually bind to (binding_host if configured, + # otherwise the client-supplied host). + probe_host = self.settings.binding_host or host + port = find_unused_port(host=probe_host) + if port is None: + msg = "Error: Could not find an unused port to initialize the server." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + standalone = init_props.standalone if init_props.standalone is not None\ + else settings.default_standalone + dark_mode = init_props.dark_mode if init_props.dark_mode is not None\ + else settings.default_dark_mode + rendering_mode = init_props.rendering_mode if init_props.rendering_mode is not None\ + else RenderingMode.LOCAL + # initialize an instance of Visor + self.visualizer, warnings = self._connect_or_initialize_visualizer( + host, + port, + standalone, + dark_mode, + rendering_mode + ) + url = self._get_url(host, port) + response = {"message": f"Set active instance to {url}"} + if warnings: + response["warnings"] = warnings + return response + + async def list_instances(self): + """List the URLs of all Visor instances in the cache.""" + return {"urls": VisorCache.list_instances()} + + @handle_exceptions + async def start_instance(self, start_props: StartProps = Body( + examples=[{ + "file_path": "path/to/file.vtk", + "metadata": {"name": "model", "unit": "m"}, + "timeout": 0 + }] + )): + """Start the active visualizer instance with the given input file and metadata.""" + if self.visualizer is None: + msg = "No active Visor instance. Please initialize the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + + # Start visualizer instance + dataset_id, task = await self.visualizer._start_async( + input=start_props.file_path, + metadata=start_props.metadata, + timeout=start_props.timeout + ) + return {"success": f"Server started on {self.visualizer.url}", + "dataset_id": dataset_id} + + @handle_exceptions + async def update(self, update_props: UpdateProps = Body( + examples=[{ + "file_path": "path/to/updated_file.vtk", + "metadata": {"name": "updated_model", "unit": "m"} + }] + )): + """ + Update the input file and metadata of the active visualizer instance, + clearing any previous datasets. + """ + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + + # Update the input file of visualizer instance + dataset_id = self.visualizer.update(update_props.file_path, update_props.metadata) + return {"success": f"Server input updated on {self.visualizer.url}", + "dataset_id": dataset_id} + + async def _stop_visualization(self): + """Stop active visualizer instance and return the URL""" + if self.visualizer is None: + logger.error("Server not initialized") + raise HTTPException(status_code=503, detail="Server not initialized") + url = self.visualizer.url + await self.visualizer._stop_async() + return url + + @handle_exceptions + async def stop_visualization(self): + """Stop active visualizer instance but keep the Visor instance in cache""" + url = await self._stop_visualization() + return {"success": f"Stopped visualization for server running on {url}"} + + @handle_exceptions + async def stop_instance(self): + """Stop visualizer server and delete Visor instance from cache""" + url = await self._stop_visualization() + VisorCache.delete_instance(url) + self.visualizer = None + return {"success": f"Stopped visualization for server running on {url} and deleted instance"} + + @handle_exceptions + async def add_dataset(self, update_props: UpdateProps = Body( + examples=[{ + "file_path": "path/to/updated_file.vtk", + "metadata": {"name": "updated_model", "unit": "m"} + }] + )): + """Add a dataset to the active visualizer instance without clearing previous datasets.""" + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + + # Add dataset to the visualizer instance + dataset_id = self.visualizer.add_dataset(update_props.file_path, update_props.metadata) + return {"success": f"Added dataset to visualizer running on {self.visualizer.url}", + "dataset_id": dataset_id} + + @handle_exceptions + async def remove_dataset(self, remove_dataset_props: RemoveDatasetProps = Body( + examples=[{ + "dataset_id": 123456 + }] + )): + """Remove a dataset from the active visualizer instance by its ID.""" + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + + # Remove dataset from the active visualizer instance + self.visualizer.remove_dataset(remove_dataset_props.dataset_id) + return {"success": f"Removed dataset with ID {remove_dataset_props.dataset_id}" + f"to visualizer running on {self.visualizer.url}" + } + + @handle_exceptions + async def list_datasets(self): + """List all datasets in the active visualizer instance.""" + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + + # List datasets loaded in the visualizer instance + return {"datasets": self.visualizer.list_datasets()} + + @handle_exceptions + async def list_variables(self, dataset_id: int): + """List variables for a dataset, grouped by part.""" + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + + # List variables for a dataset, grouped by part. + variables = self.visualizer.list_variables(dataset_id) + return {"parts": [part.to_dict() for part in variables]} + + @handle_exceptions + async def update_variables(self, dataset_id: int, variables: UpdateVariableProps): + """Update variables for a dataset, grouped by part.""" + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + + # List datasets loaded in the visualizer instance + update_variables = [var.model_dump() for var in variables.variables] + return {"variables": self.visualizer.update_variables(dataset_id, update_variables)} + + @handle_exceptions + async def save_state(self, save_props: SaveStateProps = Body( + examples=[{ + "state_dir": "path/to/state_dir" + }] + )): + """Save the current state of the visualizer instance to a directory.""" + state_dir = save_props.state_dir + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + # Save the current state of the visualizer instance to a file + await self.visualizer.save_state(state_dir) + return {"success": f"Saved state to {state_dir} for visualizer running on {self.visualizer.url}"} + + @handle_exceptions + async def load_state(self, save_props: LoadStateProps = Body( + examples=[{ + "state_dir": "path/to/state_dir" + }] + )): + """Load the current state of the visualizer instance from a directory.""" + state_dir = save_props.state_dir + if self.visualizer is None: + msg = "No active Visor instance. Please initialize and start the server first." + logger.error(msg) + raise HTTPException(status_code=503, detail=msg) + # Load the state of the visualizer instance from a file + self.visualizer.load_state(state_dir) + return {"success": f"Loaded state from {state_dir} for visualizer running on {self.visualizer.url}"} + + async def health_live(self): + """Get the health status of the FastAPI server""" + return {"status": "ok"} + + # Protected methods + def _get_url(self, host: str, port: int) -> str: + """Get the URL for a host and port""" + # Report the public hostname provided by the initializer (host). + # The server may listen on a separate binding host (settings.binding_host), + # but the URLs advertised to clients MUST use the public host. + # Prefer the Settings-provided `url_scheme` when available; fall back + # to a safe runtime check for environments where `settings` may be + # monkeypatched (e.g. unit tests using MagicMock). + scheme = self.settings.url_scheme + return f"{scheme}://{host}:{port}" + + def _connect_or_initialize_visualizer( + self, + host: str, + port: int, + standalone: bool, + dark_mode: bool, + rendering_mode: RenderingMode = RenderingMode.LOCAL, + ) -> Tuple[Visor, str]: + """Connect to a visualizer instance with the given host and port if one + exists; otherwise, create a new visualizer instance on that host and port.""" + url = self._get_url(host, port) + msg = ( + f'Connecting to Visor server on {url} (standalone={standalone}, dark_mode={dark_mode})' + ) + logger.info(msg) + # Initialize the server with the given port, host and client distribution path + instance, warnings = VisorCache.get_instance(url, rendering_mode, standalone, dark_mode, self.trame_log_dir) + return instance, warnings + + +app = VisorAPI().app diff --git a/src/ansys/visor/viewer/api/visor_cache.py b/src/ansys/visor/viewer/api/visor_cache.py new file mode 100644 index 00000000..e559db5e --- /dev/null +++ b/src/ansys/visor/viewer/api/visor_cache.py @@ -0,0 +1,124 @@ +"""Cache for Visor visualizer instances for FastAPI service""" + +from typing import Dict, Tuple + +from ansys.visor.viewer import Visor +from ansys.visor.viewer.app.visor_vtk import VisorVTK +from ansys.visor.viewer.core.visor_enums import RenderingMode +from ansys.visor.viewer.core.visor_logging import VisorLogger + +logger = VisorLogger(__name__, "server.log") + + +class VisorCache: + """A simple cache to store Visor instances by URL.""" + _instances: Dict[str, Visor] = {} + + @classmethod + def get_instance(cls, + url: str, + rendering_mode: RenderingMode, + standalone: bool, + dark_mode: bool, + trame_log_dir: str | None = None, + ) -> Tuple["Visor", str | None]: + """Get an instance of Visor from the cache or create a new one. + + Returns + ------- + tuple[Visor, list[str], bool] + The Visor instance, a list of warning messages for any params + that were ignored because the instance already existed, and a + boolean indicating whether a new instance was created. + """ + if url not in cls._instances: + msg = ( + f"Visor instance not found on url {url}. " + f"Creating a new one with rendering_mode={rendering_mode}," + f"standalone={standalone}," + f"dark_mode={dark_mode}, trame_log_dir={trame_log_dir}." + ) + logger.warning(msg) + cls.create_instance(url, rendering_mode, standalone, dark_mode, trame_log_dir) + instance = cls._instances[url] + if instance is None: + raise Exception(f"Failed to create Visor instance for url {url}.") + return instance, None + + warnings = cls.validate_args(url, rendering_mode, standalone, dark_mode, trame_log_dir) + return cls._instances.get(url), warnings + + @classmethod + def validate_args( + cls, + url: str, + rendering_mode: RenderingMode, + standalone: bool, + dark_mode: bool, + trame_log_dir: str + ) -> str: + """Validate the arguments for this instance.""" + warnings = None + existing = cls._instances[url] + ignored = [] + if rendering_mode is not None and not isinstance(existing, VisorVTK): + # TODO: update to check the VisorVtkLocal class type once VisorVTK has + # been renamed VisorVtk with VisorVtkLocal as a concrete implementation/subclass. + ignored.append( + f"rendering_mode={rendering_mode} (existing instance type: {existing.__class__.__name__})" + ) + if standalone is not None and existing.standalone != standalone: + ignored.append( + f"standalone={standalone} (existing value: {existing.standalone})" + ) + if dark_mode is not None and existing.dark_mode != dark_mode: + ignored.append( + f"dark_mode={dark_mode} (existing value: {existing.dark_mode})" + ) + if ignored: + warnings = ( + f"Connected to existing Visor instance at {url}. " + f"The following parameters were ignored because they cannot be " + f"changed on an existing instance: {', '.join(ignored)}" + ) + logger.warning(warnings) + print(warnings) + return warnings + + @classmethod + def create_instance( + cls, + url: str, + rendering_mode: RenderingMode, + standalone: bool, + dark_mode: bool, + trame_log_dir: str | None = None + ) -> None: + """Create a new instance of Visor and store it in the cache.""" + cls._instances[url] = Visor( + url=url, + rendering_mode=rendering_mode, + standalone=standalone, + dark_mode=dark_mode, + trame_log_dir=trame_log_dir, + ) + msg = ( + f"Created new Visor instance for URL: {url} " + f"(rendering_mode={rendering_mode}, standalone={standalone}, " + f"dark_mode={dark_mode}, trame_log_dir={trame_log_dir})" + ) + logger.info(msg) + print(msg) + + @classmethod + def delete_instance(cls, url: str) -> None: + """Delete an instance of Visor from the cache.""" + if url not in cls._instances: + return + cls._instances.pop(url) + return + + @classmethod + def list_instances(cls) -> list[str]: + """List URLs for all Visor instances in the cache.""" + return list(cls._instances.keys()) diff --git a/src/ansys/visor/viewer/app/__init__.py b/src/ansys/visor/viewer/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/app/trame/__init__.py b/src/ansys/visor/viewer/app/trame/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/app/trame/async_runner.py b/src/ansys/visor/viewer/app/trame/async_runner.py new file mode 100644 index 00000000..80a92a3f --- /dev/null +++ b/src/ansys/visor/viewer/app/trame/async_runner.py @@ -0,0 +1,197 @@ +"""Class that allows running coroutines in a background thread, and waiting for them +to finish synchronously.""" + +import asyncio +import re +import threading +import weakref +from asyncio import AbstractEventLoop, Future +from concurrent.futures import Future as ConcurrentFuture +from threading import Event, Thread +from typing import Coroutine + + +class AsyncRunner: + """ + `AsyncRunner` provides a non-invasive way to synchronously wait for coroutines + to finish. It does this by handing coroutines off to a dedicated background + thread, and returns a Future that completes when the coroutine is finished. + It is non-invasive because it does not modify nor use the event loop that + may be running in the main thread, nor does it create an event loop in the + main thread. + """ + + def __init__(self): + self.__error_regex_pattern = re.compile(r"\b(?:set_wakeup_fd|aiohttp)\b", re.IGNORECASE) + self.__thread: Thread | None = None + self.__loop: AbstractEventLoop | None = None + + def __ensure_thread_and_loop(self) -> None: + if self.__thread is not None: + return + loop: AbstractEventLoop | None = None + loop_set_event = Event() + startup_error: BaseException | None = None + + def thread_target(): + nonlocal loop, startup_error + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop_set_event.set() + loop.run_forever() + except BaseException as e: + startup_error = e + finally: + loop_set_event.set() + + thread = threading.Thread(target=thread_target, daemon=True) + thread.start() + while not loop_set_event.wait(timeout=0.2): + pass + if startup_error: + raise startup_error + if loop is None: + raise RuntimeError("Event loop cannot be None") + self.__thread = thread + self.__loop = loop + + @property + def thread(self) -> Thread: + """Gets the background thread.""" + self.__ensure_thread_and_loop() + return self.__thread + + async def run_coroutine_async(self, coroutine: Coroutine, patch_return_future: bool) -> any: + """ + Offloads a provided coroutine to run on a background thread asynchronously. + + Args: + coroutine (Coroutine): The coroutine to run on the background thread. + patch_return_future (bool): If `coroutine` itself returns a Future or Task object, this + specifies whether that awaitable object should be "patched" to allow + you to `await` it in the main thread. For example, if `coroutine` + itself returns a `Task` object, you will be able to `await` that `Task` + in the main thread without getting the error: `Task got Future attached + to a different loop`. + + Returns: + any: The result of the coroutine. + """ + concurrent_future: ConcurrentFuture = self.run_coroutine(coroutine, patch_return_future) + # Use asyncio.wrap_future() to convert the "concurrent.futures.Future" (which is + # non-awaitable) into an "asyncio.Future" (which is awaitable). + return await asyncio.wrap_future(concurrent_future) + + def run_coroutine_block(self, coroutine: Coroutine, patch_return_future: bool) -> any: + """ + Offloads a provided coroutine to run on a background thread. Works in both synchronous + and asynchronous contexts. Blocks the thread until the coroutine is finished. + + Args: + coroutine (Coroutine): The coroutine to run on the background thread. + patch_return_future (bool): If `coroutine` itself returns a Future or Task object, this + specifies whether that awaitable object should be "patched" to allow + you to `await` it in the main thread. For example, if `coroutine` + itself returns a `Task` object, you will be able to `await` that `Task` + in the main thread without getting the error: `Task got Future attached + to a different loop`. + + Returns: + any: The result of the coroutine. + """ + concurrent_future: ConcurrentFuture = self.run_coroutine(coroutine, patch_return_future) + condition = threading.Condition() + with condition: + while not condition.wait_for(lambda: concurrent_future.done(), timeout=0.2): + pass + e = concurrent_future.exception() + if e: + raise e + return concurrent_future.result() + + def run_coroutine(self, coroutine: Coroutine, patch_return_future: bool) -> ConcurrentFuture: + """ + Offloads a provided coroutine to run on a background thread. Works in both synchronous + and asynchronous contexts. Returns a concurrent.futures.Future that completes when the + coroutine is finished. + + Args: + coroutine (Coroutine): The coroutine to run on the background thread. + patch_return_future (bool): If `coroutine` itself returns a Future or Task object, this + specifies whether that awaitable object should be "patched" to allow + you to `await` it in the main thread. For example, if `coroutine` + itself returns a `Task` object, you will be able to `await` that `Task` + in the main thread without getting the error: `Task got Future attached + to a different loop`. + + Returns: + concurrent.futures.Future: A concurrent.futures.Future that completes when the coroutine is + finished. The result of the future is the result of the coroutine. If `patch_return_future` is + True` and `coroutine` returned a `Future` or `Task`, the result of the coroutine will be + another coroutine that can be awaited in the main thread without error. + """ + self.__ensure_thread_and_loop() + current_loop: AbstractEventLoop | None = None + try: + current_loop = asyncio.get_running_loop() + except RuntimeError: + pass + if current_loop is self.__loop: + raise RuntimeError(("The provided event loop cannot be the same as the event" + " loop associated with the current thread.")) + + async def outer_coroutine(): + result: any + try: + result = await coroutine + except BaseException as e: + for element in e.args: + if isinstance(element, str) and self.__error_regex_pattern.match(element): + # Append helpful information to the existing error message if + # the message contains keywords listed in the regex pattern. + msg = ( + "Caught an error related to 'set_wakeup_fd()' or 'aiohttp'. If you are trying to start a\n" + " Trame server, ensure you are passing the background thread to the 'server.start()'\n" + " method, like this: 'server.start(thread=async_runner.thread)'. The following is the\n" + f" original exception information:\n{e.args[0]}" + ) + e.args = (msg,) + raise + if patch_return_future: + # If the coroutine that we ran on the background thread returns a Future (this includes Tasks), + # then "patch" it so that it can be awaited in the main thread without error. We do this by + # creating a concurrent.futures.Future (a.k.a. ConcurrentFuture) object that resolves when the + # background thread's return Future resolves. We then return a coroutine that wraps the + # ConcurrentFuture into an asyncio.Future and awaits it. This allows us to await the background + # thread's Future from a different thread, without getting the "Task got Future attached to a + # different loop" error. + if not isinstance(result, Future): + raise RuntimeError("coroutine must return a Future if patch_return_future is True") + res_fut: Future = result + con_fut = ConcurrentFuture() + res_fut.add_done_callback(lambda _: con_fut.set_result(res_fut.result())) + + async def patch(): + return await asyncio.wrap_future(con_fut) + + coro = patch() + # Register the coroutine to be closed when it is garbage collected. + # This fixes the "coroutine was never awaited" warning (it's OK if the + # coroutine was never awaited). + weakref.finalize(coro, coro.close) + return coro + return result + + return asyncio.run_coroutine_threadsafe(outer_coroutine(), self.__loop) + + def dispose(self): + """Stops the background thread and closes the event loop.""" + if self.__loop is None: + return + self.__loop.call_soon_threadsafe(self.__loop.stop) + if self.__thread is not None: + self.__thread.join(timeout=2) + self.__loop.close() + self.__loop = None + self.__thread = None diff --git a/src/ansys/visor/viewer/app/trame/local_app.py b/src/ansys/visor/viewer/app/trame/local_app.py new file mode 100644 index 00000000..8d2b545c --- /dev/null +++ b/src/ansys/visor/viewer/app/trame/local_app.py @@ -0,0 +1,164 @@ +import urllib.parse +from logging import Logger + +from trame.app.core import Server +from trame.decorators import TrameApp, trigger +from vtk import vtkObject + +from ansys.visor.viewer.config import settings +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +logger = VisorDefaultLogger(__name__) + +# Disable VTK warning display +vtkObject.GlobalWarningDisplayOff() + + + +@TrameApp() +class LocalApp: + """ + LocalApp is a class that provides an interface for initializing and managing a visualization application + using the VTK rendering engine. + It supports server-based rendering and interaction with a visualization pipeline. + Attributes: + server (Server): the Trame server instance + Methods: + get_scene_details_json: returns the scene details in JSON format + save_state_response: sends the current app state as payload + pick_geometry: picks the geometry for rendering + perf_report_wasm: reports the performance of the wasm update cycle + perf_report_server_update: reports the performance of the server update cycle + set_only_cookie: sets a cookie on the server (note: Trame server only allows a single cookie header) + Protected Methods: + _cleanup(): Cleans up the active actor in the visualization pipeline. + """ + def __init__( + self, + server: Server, + get_scene_details_json: callable, + handle_save_state_response: callable, + standalone: bool = True, + trame_logger: Logger | None = None, + pick_geometry=None, + ): + self.server = server + # Callable to get the scene details in JSON format + self._get_scene_details_json = get_scene_details_json + # Callable to handle the save state response from the frontend + self._handle_save_state_response = handle_save_state_response + # Callable for sub-geometry picking (optional) + self._pick_geometry = pick_geometry + # logger for logging trame server lifecycle info + self.__trame_logger = trame_logger + + """ Set up lifecycle hooks. """ + self._register_lifecycle_hooks() + self._set_headers() + self._set_www_path(standalone) + + def _register_lifecycle_hooks(self): + """ Register server lifecycle event handlers for extra trame logging. """ + + @self.server.controller.add("on_server_start") + def server_start(_): + if self.__trame_logger: + self.__trame_logger.debug("Starting server") + + @self.server.controller.add("on_server_ready") + def server_ready(**state): + print(f"Server started and ready on {self.server.name}") + if self.__trame_logger: + self.__trame_logger.debug("Server is ready.") + + @self.server.controller.add("on_client_connected") + def client_connected(): + if self.__trame_logger: + self.__trame_logger.debug("Client connected.") + + @self.server.controller.add("on_client_exited") + def client_exited(): + if self.__trame_logger: + self.__trame_logger.debug("Client exited.") + + @self.server.controller.add("on_server_exited") + def server_exited(**state): + print("Server stopped") + if self.__trame_logger: + self.__trame_logger.debug("Server is exiting.") + + def _set_headers(self): + """ Set HTTP headers for the server.""" + # If a Dash client is connecting to this Trame server, + # the Dash client will be running on a different port, so + # we need to allow cross-origin access to it. + self.server.http_headers.set_header("Access-Control-Allow-Origin", "*") + # Always serve assets fresh (not from cache). This + # will save us some headaches in the future. + self.server.http_headers.set_header("Cache-Control", "max-age=0") + + def _set_www_path(self, standalone: bool): + """ Set the www path for the server based on the standalone flag.""" + self.server._www = settings.get_client_bundle(standalone) + logger.debug(f"Standalone client path: {self.server._www}") + + @trigger("get_visor_scene_details_json") + def get_visor_scene_details_json(self): + """ returns the scene details in JSON format. """ + return self._get_scene_details_json() + + @trigger("save_state_response") + def save_state_response(self, request_id: int, payload: dict): + """Frontend โ†’ Backend: the frontend sends the current app state as payload.""" + return self._handle_save_state_response(request_id, payload) + + @trigger("pick_geometry") + def pick_geometry(self, actor_wasm_id, cell_id, mode: str, world_x: float, world_y: float, world_z: float): + """Frontend โ†’ Backend: perform sub-geometry picking given a WASM actor/cell ID and world-space point.""" + if self._pick_geometry is None: + return {"found": False} + return self._pick_geometry(actor_wasm_id, cell_id, mode, world_x, world_y, world_z) + + @trigger("perf_report_wasm") + def perf_report_wasm(self, payload: dict): + """ + Frontend -> Backend: client ships its updateAsync perf counters after + each wasm update cycle. Written as a [PERF] line so PerfLogReader can + include client-side timing in the same JSON report. + Silently ignored when perf logging is disabled. + """ + if not settings.perf_logging: + return + c = payload + logger.info( + f"[PERF] client updateAsync" + f" | states={c.get('stateCount', 0)} ({c.get('stateTotalBytes', 0) / 1e6:.3f} MB)" + f" | blobs={c.get('blobCount', 0)} ({c.get('blobTotalBytes', 0) / 1e6:.3f} MB total" + f", largest={c.get('blobMaxBytes', 0) / 1e6:.3f} MB in {c.get('blobMaxDur', 0):.2f}ms)" + f" | wasm={c.get('wasmMs', 0):.2f}ms resize={c.get('resizeMs', 0):.2f}ms" + f" | total={c.get('totalMs', 0):.2f}ms" + ) + + @trigger("perf_report_server_update") + def perf_report_server_update(self, payload: dict): + """ + Frontend -> Backend: client ships the onServerUpdateAsync duration โ€” + the React UI rebuild triggered by the trame state change, after the + wasm cycle completes. + Silently ignored when perf logging is disabled. + """ + if not settings.perf_logging: + return + logger.info( + f"[PERF] client onServerUpdateAsync" + f" | handler={payload.get('handlerMs', 0):.2f}ms" + ) + + def set_only_cookie(self, key: str, value: str): + """ + Sets a cookie on the server. NOTE: there is a limitation + with Trame server whereby it only allows you to set a single cookie header. + """ + key = urllib.parse.quote(key) + value = urllib.parse.quote(value) + self.server.http_headers.set_header("Set-Cookie", f"{key}={value};Path=/;") diff --git a/src/ansys/visor/viewer/app/trame/trame_server_manager.py b/src/ansys/visor/viewer/app/trame/trame_server_manager.py new file mode 100644 index 00000000..ee942c74 --- /dev/null +++ b/src/ansys/visor/viewer/app/trame/trame_server_manager.py @@ -0,0 +1,293 @@ +"""Class to manage the Trame server lifecycle.""" + +import asyncio +from asyncio import Task +from typing import Awaitable + +from trame.app import get_server +from trame.app.core import Server + +import ansys.visor.viewer.core.errors as errors +from ansys.visor.viewer.app.trame.async_runner import AsyncRunner +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger, get_trame_log_path + +"""Logging configuration""" +logger = VisorDefaultLogger(__name__) +from ansys.visor.viewer.config import settings + + +class TrameServerManager: + """ + `TrameServerManager` provides a synchronous and asynchronous interface + for starting and stopping a Trame server. It uses `AsyncRunner` to + run the server on a dedicated background thread, allowing the main + thread to remain responsive. + Attributes: + server (Server): The Trame server instance to control. + _host (str): The host address for the server (default is "localhost"). + _port (int): The port number for the server (default is 8080). + __async_runner (AsyncRunner): An instance of AsyncRunner to manage + asynchronous execution. + Methods: + start(timeout: int = 0, blocking: bool = False) -> None: + Starts the server synchronously. + stop() -> None: + Stops the server synchronously. + start_async(timeout: int = 0, blocking: bool = False) -> Awaitable + Starts the server asynchronously. + stop_async() -> None: + Stops the server asynchronously. + Protected Methods: + _start_core(timeout: int = 0, blocking: bool = False) -> Task + Core coroutine to start the server. + _stop_core() -> None + Core coroutine to stop the server. + + """ + def __init__( + self, + host: str = "localhost", + port: int = 8080, + trame_log_dir: str | None = None + ): + """Initialize the Trame server manager.""" + self._host = host + self._port = port + self._server = self._initialize_server(trame_log_dir) + self._async_runner = AsyncRunner() + + @property + def running(self) -> bool: + """Return whether the server is running.""" + return self._server is not None and self._server.running + + @property + def server(self) -> Server: + """Return the Trame server instance.""" + return self._server + + @property + def state(self): + """ + VisorState is not yet defined. + """ + raise NotImplementedError + + @property + def url(self) -> str: + """Return the URL of the server.""" + scheme = settings.url_scheme + return f"{scheme}://{self._host}:{self._port}" + + @property + def health(self) -> bool: + """Return whether the server is running.""" + if self._server is None: + logger.error("Server not initialized") + raise RuntimeError("Server not initialized") + if self._server.controller is None: + logger.error("Server controller not initialized") + raise RuntimeError("Server controller not initialized") + return self._server.running + + def start(self, timeout: int = 0, blocking: bool = False) -> None: + """ + Start the server synchronously. + + At a minimum, this method will block until the server has started. + If blocking is True, this method will continue blocking after the server has started, + and only return when the server has stopped. + Args: + timeout (int): The timeout in seconds for starting the server. + A value of 0 means no timeout. Default is 0. + blocking (bool): If True, the call will block until the server + stops. Default is False. + Raises: + InvalidServerTimeoutError: If the provided timeout is negative. + RuntimeError: If the server fails to start for any reason. + """ + coroutine = self._start_core(timeout, blocking) + self._async_runner.run_coroutine_block(coroutine, True) + + def stop(self): + """ + Stop the server synchronously. + Raises: + RuntimeError: If the server fails to stop for any reason. + """ + coroutine = self._stop_core() + self._async_runner.run_coroutine_block(coroutine, False) + + async def start_async(self, timeout: int = 0, blocking: bool = False) -> Awaitable: + """ + Start the server asynchronously. + + If this method is awaited, it will do one of two things depending on whether + blocking is True or False: + 1. If blocking is False, awaiting this method will only block until the + underlying Trame server is started, and then promptly return an Awaitable object + representing the Trame server. You may then await this object later on in your code + to block until the server is stopped. + 2. If blocking is True, awaiting this method will block until the underlying Trame + server is started, and then continue blocking until the server is stopped. + + Args: + timeout (int): The timeout in seconds for starting the server. + A value of 0 means no timeout. Default is 0. + blocking (bool): If True, the coroutine will not complete until + the server stops. Default is False. + Returns: + Awaitable: An awaitable that completes when the server has started. + Raises: + InvalidServerTimeoutError: If the provided timeout is negative. + RuntimeError: If the server fails to start for any reason. + """ + coroutine = self._start_core(timeout, blocking) + return await self._async_runner.run_coroutine_async(coroutine, True) + + async def stop_async(self) -> None: + """ + Stop the server asynchronously. + Raises: + RuntimeError: If the server fails to stop for any reason. + """ + coroutine = self._stop_core() + await self._async_runner.run_coroutine_async(coroutine, False) + + async def _start_core(self, + timeout: int = 0, + blocking: bool = False + ) -> Task: + """ + Core coroutine to start the server. + Args: + timeout (int): The timeout in seconds for starting the server. + A value of 0 means no timeout. Default is 0. + blocking (bool): If True, the coroutine will not complete until + the server stops. Default is False. + Returns: + Task: The asyncio Task representing the server. + Raises: + InvalidServerTimeoutError: If the provided timeout is negative. + RuntimeError: If the server fails to start for any reason. + """ + print("Server starting...") + + logger.debug("Starting server on main thread") + + """Start the server on the main thread""" + if timeout and timeout < 0: + raise errors.InvalidServerTimeoutError(timeout) + + server_coroutine = self._server.start( + # Specify the background thread here, or else you will + # get the error "set_wakeup_fd only works in main thread + # of the main interpreter". + thread=self._async_runner.thread, + host=self._host, + port=self._port, + # Start as coroutine in case we need to + # see Trame server console logging (the + # server console logging only works if + # exec_mode is "coroutine"). + exec_mode="coroutine", + open_browser=False, + show_connection_info=False, + timeout=timeout, + ) + + if server_coroutine is None: + # If you try start a Trame server with the same host and + # port as an existing Trame server running on the same thread, + # server.start() will return None instead of throwing a port + # conflict error. Port conflict errors are only thrown if the + # existing Trame server is running on a different thread. + msg = "Could not create server coroutine. Are you using the" + msg += " same host and port as an existing server instance?" + logger.error(msg) + raise RuntimeError(msg) + + # Start the server by turning the server coroutine into a task. + server_task = asyncio.create_task(server_coroutine) + + done, pending = await asyncio.wait( + [self._server.ready, server_task], + timeout=10, + return_when=asyncio.FIRST_COMPLETED, + ) + + if self._server.ready in done: + e = self._server.ready.exception() + if e is not None: + raise e + if not self._server.ready: + # If we make it here, server.ready was set, but it is false for + # some reason (server.start() failed but didn't throw an exception). + msg = "Server could not start (unknown cause)" + logger.error(msg) + raise RuntimeError(msg) + elif server_task in done: + e = server_task.exception() + if e is not None: + raise e + if not self._server.ready: + # If we make it here, the server task finished without exceptions + # before server.ready got set. This should never happen. + msg = "Server could not start (unknown cause)" + logger.error(msg) + raise RuntimeError(msg) + else: + # If we make it here, server.ready never got set, and no + # server exception was thrown (we timed out for an unknown reason). + msg = "Timed out while starting server." + logger.error(msg) + raise RuntimeError(msg) + + if blocking: + await server_task + + # Success! We can now return the actual server task in all its glory. + return server_task + + # Protected Methods + async def _stop_core(self): + """ + Core coroutine to stop the server. + Raises: + RuntimeError: If the server fails to stop for any reason. + """ + logger.debug("Stopping server on main thread") + if self._server.running: + await self._server.stop() + # TODO: Trame's 'on_server_exited' handler never runs (might + # be bug), so log 'Server stopped' here. Remove this + # when the handler is fixed. BHB 2025-09-10 + print("Server stopped") + + def _initialize_server(self, trame_log_dir: str | None = None) -> Server: + """Initialize the server instance based on the input server or url""" + + logger.debug( + f"Initializing server with url {self.url}, hostname " + f"{self._host} and port {self._port}" + ) + server = get_server(name=self.url, log_network=get_trame_log_path(trame_log_dir)) + + # Platform-native TLS support (optional): if the platform provides + # a pre-resolved SSL certificate path (from Settings), configure + # wslink/trame to use it so the websocket endpoint speaks WSS. + try: + ssl_val = getattr(settings, "ssl_certificate", None) + if isinstance(ssl_val, str) and ssl_val.strip(): + # Only attempt to set defaults if the server exposes a CLI + if hasattr(server, "cli") and hasattr(server.cli, "set_defaults"): + try: + server.cli.set_defaults(ssl=ssl_val) + logger.debug(f"Configured Trame/wslink SSL using {ssl_val}") + except Exception: + logger.exception("Failed to set trame CLI ssl defaults") + except Exception: + # Don't fail server initialization if TLS auto-config fails + logger.exception("Error while attempting to configure Trame SSL from settings") + + return server \ No newline at end of file diff --git a/src/ansys/visor/viewer/app/visor.py b/src/ansys/visor/viewer/app/visor.py new file mode 100644 index 00000000..89a400fc --- /dev/null +++ b/src/ansys/visor/viewer/app/visor.py @@ -0,0 +1,278 @@ +import urllib +from asyncio import Task +from typing import Any, Awaitable, Dict, List, Type + +import ansys.visor.viewer.core.errors as errors +from ansys.visor.viewer.config import settings +from ansys.visor.viewer.core.metadata import Metadata +from ansys.visor.viewer.core.visor_enums import RenderingEngine, RenderingMode +from ansys.visor.viewer.core.visor_helpers import validate_url +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables + +logger = VisorDefaultLogger(__name__) + + + +class Visor: + """ + Base class that provides an interface for initializing and managing a + visualization app. + + This class supports server-based rendering and interaction with a visualization pipeline. + + Parameters + ---------- + rendering_engine : RenderingEngine, default: RenderingEngine.VTK + Rendering engine to use for visualization. + url : str, default: None + URL to connect to an existing server instance or to create a new one. + standalone : bool, default: True + Whether to use the standalone client path or + the Dash component in a Dash app. + dark_mode : bool, Default: None + Whether to use dark mode for the app. If ``None``, + the default dark mode setting from the configuration is used. + """ + def __new__(cls, + rendering_engine: RenderingEngine = RenderingEngine.VTK, + rendering_mode: RenderingMode = RenderingMode.LOCAL, + url: str | None = None, + standalone: bool = True, + dark_mode: bool | None = None, + **kwargs, + ) -> "Visor": + """ + Validates the rendering engine and returns a Visor instance. + + This is a factory method to return an instance of a subclass + that is determined by the ``rendering_engine`` enum value provided as input. + """ + if cls is not Visor: + return super().__new__(cls) + + rendering_engine = RenderingEngine.from_string(rendering_engine) + rendering_mode = RenderingMode.from_string(rendering_mode) + + from ansys.visor.viewer.app.visor_vtk import VisorVTK + + engine_map: dict[RenderingEngine, Type[Visor]] = { + RenderingEngine.VTK: VisorVTK, + } + + engine_class = engine_map.get(rendering_engine, None) + + if not engine_class: + raise ValueError(( + f"Unsupported rendering engine: {rendering_engine}. " + f"Valid values: {list(engine_map.keys())}." + )) + # Delegate to the engine class's __new__, passing rendering_mode through. + return engine_class.__new__(engine_class, rendering_mode=rendering_mode, **kwargs) + + def __init__( + self, + url: str | None = None, + standalone: bool = True, + dark_mode: bool | None = None, + ): + """ + Initializes the Visor instance with the provided parameters. + """ + self._set_default_url(url) + self._standalone: bool = standalone + self._dark_mode: bool = dark_mode if dark_mode is not None else settings.default_dark_mode + logger.debug(f"Dash flag: {self._standalone}") + + """ Properties """ + @property + def url(self): + """URL of the server.""" + return self._url + + @property + def host(self): + """Host of the server.""" + return self._host + + @property + def port(self): + """Port of the server.""" + return self._port + + @property + def standalone(self): + """Dash flag of the server""" + return self._standalone + + @property + def dark_mode(self): + """Dark mode flag of the server""" + return self._dark_mode + + """ Public Methods """ + def info(self) -> dict: + """Return a dictionary with information about the VISOR instance.""" + return { + "app_name": settings.app_name, + "host": self.host, + "port": self.port, + "standalone": self.standalone, + "datasets": [dataset.get("name") for dataset in self.list_datasets().values()], + "metadata": getattr(self, "_metadata", None), + } + + """ Synchronous Abstract Methods """ + def start(self, + input: str | VisorDatasetType | None = None, + metadata: Metadata | None = None, + timeout: int = 0, + blocking: bool = False, + ) -> int | None: + """ Start the server and app synchronously. """ + raise NotImplementedError() + + def stop(self) -> None: + """Stop the server. """ + raise NotImplementedError() + + def update(self, + input: str | VisorDatasetType, + metadata: Metadata | None = None + ) -> int: + """Updates the model with a new input file and metadata. """ + raise NotImplementedError() + + def save_state(self, state_dir: str) -> None: + """Save the app state to a JSON file. """ + raise NotImplementedError() + + def load_state(self, state_dir: str) -> None: + """Load the app state from a JSON file. """ + raise NotImplementedError() + + def _health(self) -> None: + """Return the health status of the server. """ + raise NotImplementedError() + + + """ Protected Async Abstract Methods (for use in the FastAPI endpoints) """ + async def _start_async( + self, input: str | VisorDatasetType | None = None, + metadata: Metadata | None = None, + timeout: int = 0) -> tuple[int | None, Awaitable]: + """Start the server and app asynchronously. """ + raise NotImplementedError() + + async def _stop_async(self) -> Task: + """Stop the server asynchronously. """ + raise NotImplementedError() + + def add_dataset(self, + input: str | VisorDatasetType, + metadata: Metadata | None = None + ) -> int: + """Add a dataset to the scene and return its dataset ID.""" + raise NotImplementedError + + def remove_dataset(self, dataset_id: int) -> None: + """Remove a dataset from the scene.""" + raise NotImplementedError + + def list_datasets(self) -> dict: + """List the available datasets in the scene.""" + raise NotImplementedError + + def list_variables(self, dataset_id: int) -> List[VisorPartVariables]: + """ + List the variables for a dataset. + + Parameters + ---------- + dataset_id : int + ID of the dataset to query. + + Returns + ------- + List[VisorVariable] + List of variables available for the specified dataset. + + Raises + ------ + RuntimeError + If the variables cannot be listed. + """ + raise NotImplementedError + + def update_variables(self, dataset_id: int, variables: List[Dict[str, Any]]) -> None: + """ + Update variables for a dataset. + + Parameters + ---------- + dataset_id : int + ID of the dataset to update. + variables : List[Dict[str, Any]] + List of dictionaries describing variable updates. + + Raises + ------ + RuntimeError + If the update operation fails. + + Notes + ----- + Each dictionary must include: + + - ``type`` (str or VisorVtkVariableType): "POINT" or "CELL" + - ``name`` (str): Non-empty variable name + - ``num_components`` (int): Number of components (> 0) + - ``data`` (array-like): List, tuple, or numpy.ndarray shaped + as (num_points, num_components) or a flat list divisible by ``num_components``. + + """ + raise NotImplementedError + + """ Protected Methods """ + def _set_default_url(self, url: str | None) -> None: + """Validate and parse the provided URL, set as default if None""" + if url is None: + scheme = settings.url_scheme + url = f"{scheme}://{settings.default_host}:{settings.default_port}" + logger.debug(f"Initializing server with default url {url}") + self._host = settings.default_host + self._port = settings.default_port + # Exposed/binding host may be provided by evaluator via settings.binding_host + self._binding_host = settings.binding_host or self._host + # Advertised URL should use the public host (self._host), not the binding/listen host + self._url = f"{scheme}://{self._host}:{self._port}" + return + + self._set_url(url) + + def _set_url(self, url: str) -> None: + """Validate and parse the provided URL""" + if url is None: + logger.error(f"Null url provided: {url}") + raise errors.InvalidUrlError(f"Null url provided: {url}") + + logger.debug(f"Initializing server with url {url}") + + path = urllib.parse.urlparse(url) + try: + validate_url(url) + except TypeError: + logger.error(f"Invalid url provided: {url}") + raise errors.InvalidUrlError(url) + if validate_url is None: + logger.error(f"Invalid url provided: {url}") + raise errors.InvalidUrlError(url) + + self._host = path.hostname + self._port = int(path.port) + # Exposed/binding host may be provided by evaluator via settings.binding_host + self._binding_host = settings.binding_host or self._host + # Advertised URL should use the public host (self._host) and reflect TLS availability + scheme = settings.url_scheme + self._url = f"{scheme}://{self._host}:{self._port}" diff --git a/src/ansys/visor/viewer/app/visor_vtk.py b/src/ansys/visor/viewer/app/visor_vtk.py new file mode 100644 index 00000000..8e155127 --- /dev/null +++ b/src/ansys/visor/viewer/app/visor_vtk.py @@ -0,0 +1,593 @@ +"""Trame VTKlocal implementation of Visor class""" + +import asyncio +import traceback +from abc import abstractmethod +from functools import wraps +from pathlib import Path +from typing import Any, Awaitable, Dict, List + +from trame.app.core import Server + +import ansys.visor.viewer.core.errors as errors +from ansys.visor.viewer import Visor +from ansys.visor.viewer.app.trame.trame_server_manager import TrameServerManager +from ansys.visor.viewer.app.visor import Metadata +from ansys.visor.viewer.core.perf_timer import PerfTimer +from ansys.visor.viewer.core.visor_enums import RenderingMode +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 +from ansys.visor.viewer.vtk.io.visor_file_io import VisorFileIO +from ansys.visor.viewer.vtk.scene.local_scene import VisorLocalScene +from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables +from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate + +logger = VisorDefaultLogger(__name__) + + +def validate_input_metadata_types(func): + """Decorator to validate input and metadata types.""" + @wraps(func) + def wrapper(self, input=None, metadata=None, *args, **kwargs): + if input is not None and not isinstance(input, (str, VisorDatasetType)): + raise TypeError(f"input must be a str or VisorDatasetType, got {type(input).__name__}") + # Validate metadata type if provided + if metadata is not None and not isinstance(metadata, (str, Metadata)): + raise TypeError(f"metadata must be a string, an instance of {Metadata.__name__} or None; got {type(metadata).__name__}") + return func(self, input, metadata, *args, **kwargs) + return wrapper + +def require_input_is_not_none(func): + """Decorator to ensure that the input argument is not None.""" + @wraps(func) + def wrapper(self, input=None, *args, **kwargs): + if input is None: + raise ValueError("Input cannot be None") + return func(self, input, *args, **kwargs) + return wrapper + +def require_server_off(func): + """Decorator to ensure that the server is off.""" + @wraps(func) + def wrapper(self, *args, **kwargs): + if self.running: + raise RuntimeError("Visor server already running") + return func(self, *args, **kwargs) + return wrapper + +def require_server_on(func): + """Decorator to ensure that the server is on.""" + @wraps(func) + def wrapper(self, *args, **kwargs): + if not self.running: + logger.error("Failed to find running server, try starting the server first") + raise errors.ServerNotStartedError( + "Failed to find running server, try starting the server first" + ) + return func(self, *args, **kwargs) + return wrapper + +def handle_errors(action: str = "operation"): + """Decorator to handle server errors.""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + tb_str = traceback.format_exc() + logger.exception(f"Error during {action}:\n{tb_str}") + raise RuntimeError(f"Failed to {action}\n{tb_str}") from e + return wrapper + return decorator + + +class VisorVTK(Visor): + """ + VisorVTK is a class that provides an interface for initializing and managing a + visualization application using the VTK rendering engine. + It supports server-based rendering and interaction with a visualization pipeline. + + Public methods: + start(input, metadata, timeout, blocking): Starts the server and application. + stop(): Stops the server. + update(input, metadata): Clears the scene and loads a new dataset. + add_dataset(input, metadata): Adds a dataset to the current scene. + remove_dataset(dataset_id): Removes a dataset from the current scene. + list_datasets(): Lists all datasets currently in the scene. + clear(): Clears the scene and resets the visualization pipeline. + list_variables(dataset_id): Lists variables for a dataset. + update_variables(dataset_id, variables): Updates variables for a dataset. + save_state(state_dir): Saves the application state to disk. + load_state(state_dir): Loads the application state from disk. + + Protected methods (for use in FastAPI endpoints): + _start_async(input, metadata, timeout): Starts the server asynchronously. + _stop_async(): Stops the server asynchronously. + _health(): Returns the health status of the server. + + """ + + """ + Abstract base for all VTK-backed Visor implementations. + + Do not instantiate directly โ€” use ``Visor(rendering_engine=RenderingEngine.VTK, + rendering_mode=<mode>)`` or the short form ``Visor(rendering_mode=<mode>)``. + + Subclasses must implement :meth:`_initialize_rendering` to wire up the + mode-specific scene and trame app. All public API methods, error handling, + decorators, and performance instrumentation live here. + """ + + def __new__( + cls, + rendering_mode: RenderingMode = RenderingMode.LOCAL, + **kwargs, + ) -> "VisorVTK": + # If we are already constructing a concrete subclass, skip dispatch. + if cls is not VisorVTK: + return super().__new__(cls) + + rendering_mode = RenderingMode.from_string(rendering_mode) + + from ansys.visor.viewer.app.visor_vtk_local import VisorVTKLocal + + mode_map: dict[RenderingMode, type] = { + RenderingMode.LOCAL: VisorVTKLocal, + } + + subclass = mode_map.get(rendering_mode) + if subclass is None: + valid = list(mode_map.keys()) + raise ValueError( + f"Unsupported rendering mode for VTK: {rendering_mode!r}. " + f"Valid values: {valid}" + ) + return super().__new__(subclass) + + + def __init__( + self, + url: str | None = None, + rendering_mode: RenderingMode = RenderingMode.LOCAL, + standalone: bool = True, + dark_mode: bool | None = None, + trame_log_dir: str | None = None, + ): + """ + Initializes the Visor class. + Args: + url (Optional[str]): A URL that can be used to connect to an existing + server instance. Defaults to None. + standalone (bool): A flag indicating whether to use the standalone client + path or the dash component in a Dash application. Defaults to False. + dark_mode (bool): A flag indicating whether to use the Dark mode + trame_log_dir (Optional[str]): The path to the Trame log file. Defaults to None. + If set to a value, Trame logging is enabled (is disabled by default). + """ + super().__init__(url, standalone, dark_mode) + + # I/O helper โ€” owns all filesystem interactions (e.g. reading datasets, reading/writing state) + self._file_io = VisorFileIO() + + # Initialize the server + self._server_manager = self._initialize_server(trame_log_dir) + + # Delegate scene + trame-app construction to the subclass so that the + # correct rendering-mode objects are created in the right order. + self._initialize_rendering(standalone, trame_log_dir) + + @abstractmethod + def _initialize_rendering(self, standalone: bool, trame_log_dir: str | None) -> None: + """ + Construct ``self._scene`` and the trame app. + + Called from ``__init__`` after ``_server_manager`` and ``_file_io`` are in place. + """ + + """ Properties """ + @property + def running(self) -> bool: + """Returns whether the server is running.""" + return self._server_manager.running + + @property + def server(self) -> Server: + """Returns the Trame server instance.""" + return self._server_manager.server + + @property + def dataset_count(self) -> int: + """Returns the number of datasets currently in the scene.""" + return self._scene.dataset_count + + # Public Methods (Python synchronous APIs) + @require_server_off + @validate_input_metadata_types + def start(self, + input: str | VisorDatasetType | None = None, + metadata: Metadata | None = None, + timeout: int = 0, + blocking: bool = False + ) -> int | None: + """ + Start the server synchronously. + Does not block unless blocking is True. + """ + dataset_id = None + + logger.info("Starting server") + + # Clear the existing scene if new input is provided + if input is not None: + self._scene.clear() + + # Prepare the scene with the raw data + dataset_id = self._prepare_and_render_scene(input, metadata) + + # Start the Trame server + self._server_manager.start(timeout, blocking) + + return dataset_id + + @require_server_on + def stop(self): + self._server_manager.stop() + + @handle_errors("update model") + @require_server_on + @require_input_is_not_none + @validate_input_metadata_types + def update(self, + input: str | VisorDatasetType, + metadata: Metadata | None = None + ) -> int: + """ + Clear old datasets and update the scene with the new VTK dataset. + This function is going to remove all existing functions and add the new actor + based on the input model. + """ + logger.info("Updating model") + + # Clear the existing scene + self._scene.clear() + + # Prepare the scene with the raw data + dataset_id = self._prepare_and_render_scene(input, metadata) + + if dataset_id is None: + raise RuntimeError("Failed to update model: dataset_id is None") + + logger.info(f"Model updated successfully with metadata {metadata}") + + return dataset_id + + @handle_errors("add_dataset") + @require_input_is_not_none + @validate_input_metadata_types + def add_dataset(self, + input: str | VisorDatasetType, + metadata: Metadata | None = None + ) -> int: + """ + Add a dataset to the current scene without clearing existing actors. + This function loads the new dataset and adds it to the existing scene. + """ + logger.info("Adding dataset") + + timer = PerfTimer("add_dataset", logger) + dataset_id = self._prepare_and_render_scene(input, metadata, timer=timer) + timer.log(include_total=True) + + if dataset_id is None: + raise RuntimeError("Failed to update model: dataset_id is None") + + logger.info(f"Dataset added successfully with metadata {metadata} to instance {self.url}") + + return dataset_id + + @handle_errors("remove_dataset") + def remove_dataset(self, dataset_id: int) -> None: + """ + Remove a dataset from the current scene based on its dataset ID. + """ + self._scene.remove_dataset(dataset_id) + if self.dataset_count == 0: + # if we've removed the last dataset, reset widgets and camera so the blank scene is normalized + self.clear() + else: + self._scene.update_widgets() + self._scene.render() + logger.info(f"Dataset {dataset_id} successfully removed from instance {self.url}") + + @handle_errors("list_datasets") + def list_datasets(self) -> dict: + """ + List all datasets currently in the scene. + Returns a dictionary with dataset IDs as keys and metadata dictionary + (excluding the state) as values. + """ + return self._scene.list_all_dataset_info() + + @handle_errors("clear") + def clear(self): + """ + Clear the current scene and reset the visualization pipeline. + This function removes all actors from the scene and resets the state. + """ + logger.info("Clearing the scene") + self._scene.clear() + self._scene.finalize_scene() + logger.info("Scene cleared successfully") + + @handle_errors("list_variables") + def list_variables(self, dataset_id: int) -> List[VisorPartVariables]: + """ + List the variables of a specific dataset in the scene, grouped by part. + Args: + dataset_id (int): The ID of the dataset to query. + Returns: + List[VisorPartVariables]: Per-part variable listings. For non-composite + datasets this is a single-element list. The part_id in each entry is + the value to pass to update_variables when targeting that part. + """ + return self._scene.list_variables_for_dataset(dataset_id) + + @handle_errors("update_variables") + def update_variables(self, dataset_id: int, variables: List[Dict[str, Any]]) -> None: + """ + Update the variables of a specific dataset in the scene. + + Args: + dataset_id (int): The ID of the dataset to update. + variables (List[Dict[str, Any]]): A list of variable update dictionaries. + Each dictionary must have: + - type (str or VisorVtkVariableType): "POINT" or "CELL" + - name (str): Variable name (non-empty) + - num_components (int): Number of components (>0) + - data (array-like): Data as a list, tuple, or np.ndarray, + shape (num_points, num_components) or flat list divisible by num_components + + Raises: + RuntimeError: If the update operation fails. + """ + timer = PerfTimer( + "update_variables", logger, + dataset=dataset_id, + n_vars=len(variables), + ) + with timer.phase("from_dict"): + variable_updates = [VisorVariableUpdate.from_dict(var) for var in variables] + with timer.phase("update_variables_for_dataset"): + self._scene.update_variables_for_dataset(dataset_id, variable_updates) + timer.log(include_total=True) + logger.info(f"Variables for dataset {dataset_id} updated successfully") + + @require_server_on + async def save_state(self, state_dir: str, timeout: float = 5.0) -> None: + """ + Save the current viewer state to a directory. + Requests the runtime state from the frontend, converts it to persisted state, + and writes it as visor.json to state_dir. + + The canonical snapshot path is always stamped onto each dataset's persisted + state before writing, so load_state has one unambiguous place to look + regardless of how the dataset was originally loaded. + """ + try: + state = await self._scene.get_state(timeout=timeout) + except asyncio.TimeoutError as e: + msg = ( + "Timed out waiting for the frontend to respond to `getState`.\n" + "This typically means the Trame client is not connected/ready, or JS calls are not being processed.\n" + f"Waited {timeout} seconds." + ) + raise RuntimeError(msg) from e + + # Stamp the canonical snapshot path for every registered dataset. + # The registry is the source of truth for which datasets are loaded โ€” not + # the frontend state, which only knows about visual/UI properties. + for dataset_name, dataset_state in state.scene.dataset_states.items(): + dataset_state.serialized_dataset_path = str( + self._file_io.get_persisted_dataset_path(state_dir, dataset_name) + ) + + # Write each in-memory dataset to disk as a VTKHDF snapshot. + # Only datasets that are dirty (new or mutated since last save) are written. + for dataset in self._scene.datasets.values(): + if not dataset.is_dirty: + continue + snapshot_path = self._file_io.get_persisted_dataset_path(state_dir, dataset.name) + self._file_io.write_dataset(snapshot_path, dataset.data) + dataset.mark_clean() + + self._file_io.write_state(state_dir, state) + self._file_io.delete_stale_snapshots(state_dir, state) + + @handle_errors("load state") + def load_state(self, state_dir: str) -> None: + """ + Load viewer state from a directory. + Reads visor.json from state_dir and applies it to the scene. + + If the dataset registry is currently empty, each dataset whose + ``serialized_dataset_path`` exists on disk is loaded from that snapshot + before the UI state is applied. The ``ExtendedMetadata`` for each dataset + is constructed from the persisted scene state so that ``name``, ``unit``, + original source paths, and per-part state are all restored correctly. + Datasets loaded this way start with ``is_dirty=False`` โ€” the snapshot on + disk already reflects their content. + """ + state = self._file_io.read_state(state_dir) + + if self._scene.dataset_count == 0: + self._load_datasets_from_state(state) + + self._scene.apply_state(state) + + def _load_datasets_from_state(self, state: "PersistedViewerStateV1") -> None: + """Load all serialized dataset snapshots described in *state* into the scene. + + Only datasets whose ``serialized_dataset_path`` is set and points at an + existing file are loaded. Missing snapshots are skipped with a warning so + that a partial restore does not block loading the UI state. + + Loaded datasets are immediately marked clean โ€” the snapshot on disk is + already up-to-date. + """ + unit = state.scene.unit + for dataset_name, dataset_state in state.scene.dataset_states.items(): + snapshot_path = dataset_state.serialized_dataset_path + if not snapshot_path: + logger.warning( + f"No serialized_dataset_path for dataset '{dataset_name}' โ€” skipping dataset restore." + ) + continue + if not Path(snapshot_path).exists(): + logger.warning( + f"Snapshot not found for dataset '{dataset_name}' at '{snapshot_path}' โ€” skipping dataset restore." + ) + continue + + metadata = self._file_io.build_metadata_for_load_state(dataset_name, unit, dataset_state) + data = self._file_io.read_snapshot(snapshot_path) + dataset_id = self._scene.add_dataset(data, metadata) + + # Mark clean: the snapshot on disk matches what was just loaded. + dataset = self._scene.datasets.get(dataset_id) + if dataset is not None: + dataset.mark_clean() + + if self._scene.dataset_count > 0: + self._scene.finalize_scene(skip_reset_camera=True) + + """ Protected Methods (async start and stop for FastAPI) """ + @require_server_off + @validate_input_metadata_types + async def _start_async(self, + input: str | VisorDatasetType | None = None, + metadata: Metadata | None = None, + timeout: int = 0, + ) -> tuple[int | None, Awaitable]: + """ + Start the server asynchronously. Returns + a coroutine that represents the Trame server Task. + """ + logger.info("Starting server") + + # Clear the existing scene if new input is provided + if input is not None: + self._scene.clear() + + # Prepare the scene with the raw data + dataset_id = self._prepare_and_render_scene(input, metadata) + + server_task = await self._server_manager.start_async(timeout) + return dataset_id, server_task + + @require_server_on + async def _stop_async(self): + """Stop the server asynchronously.""" + await self._server_manager.stop_async() + + def _health(self) -> bool: + """Return the server health""" + logger.info("Returning server state") + return self._server_manager.health + + # Private Methods + def _initialize_server(self, trame_log_dir: str | None) -> TrameServerManager: + """Initialize the Trame server manager.""" + # Start Trame listening on the configured binding host (or the instance host if unset) + listen_host = getattr(self, "_binding_host", None) or self._host + return TrameServerManager( + listen_host, + self._port, + trame_log_dir, + ) + + def _initialize_scene(self) -> VisorLocalScene: + return VisorLocalScene(self.server, dark_mode=self._dark_mode) + + def _prepare_and_render_scene( + self, + input: str | VisorDatasetType | None = None, + metadata: Metadata | None = None, + timer: PerfTimer = None, + ) -> int | None: + """ + Register a dataset (if provided) then finalize the scene. + + Parameters + ---------- + timer : PerfTimer + Timer passed in by the caller. ``timer.phase()`` is a no-op when + perf logging is disabled, so no branching is needed here. + + Returns + ------- + int | None + The dataset_id if a dataset was added, otherwise None. + """ + if timer is None: + timer = PerfTimer.disabled() + dataset_id = self._register_dataset(input, metadata, timer=timer) + + with timer.phase("finalize_scene"): + self._scene.finalize_scene() + + return dataset_id + + def _register_dataset( + self, + input: str | VisorDatasetType | None = None, + metadata: Metadata | None = None, + timer: PerfTimer = None, + ) -> int | None: + """ + Resolve I/O and register the dataset with the scene. + Does **not** populate actors, reset the camera, or render. + + Parameters + ---------- + timer : PerfTimer + Timer passed in by the caller. ``timer.phase()`` is a no-op when + perf logging is disabled, so no branching is needed here. + + Returns + ------- + int | None + The dataset_id if a dataset was registered, otherwise None. + + Raises + ------ + errors.VisorAddDatasetError + If a dataset was provided but could not be added to the scene. + """ + if timer is None: + timer = PerfTimer.disabled() + with timer.phase("resolve_io"): + loaded_input, loaded_metadata = self._file_io.resolve(input, metadata) + + if loaded_input is None: + return None + + try: + with timer.phase("add_to_scene"): + dataset_id = self._scene.add_dataset(loaded_input, loaded_metadata) + except Exception as e: + raise errors.VisorAddDatasetError( + "Failed to add dataset to scene", + input_path=getattr(loaded_metadata, "file_path", None), + metadata_path=getattr(loaded_metadata, "metadata_path", None), + ) from e + + if dataset_id is None or (isinstance(dataset_id, int) and dataset_id < 0): + raise errors.VisorAddDatasetError( + f"Failed to add dataset to scene: invalid dataset_id returned ({dataset_id})", + input_path=getattr(loaded_metadata, "file_path", None), + metadata_path=getattr(loaded_metadata, "metadata_path", None), + ) + + return dataset_id diff --git a/src/ansys/visor/viewer/app/visor_vtk_local.py b/src/ansys/visor/viewer/app/visor_vtk_local.py new file mode 100644 index 00000000..154aa70e --- /dev/null +++ b/src/ansys/visor/viewer/app/visor_vtk_local.py @@ -0,0 +1,56 @@ +""" +VisorVTKLocal +============= +A :class:`~ansys.visor.viewer.app.visor_vtk.VisorVTK` subclass selected when +``rendering_mode=RenderingMode.LOCAL``. + +This implementation uses **VTK-WASM** (via trame-vtklocal / LocalView) for +client-side rendering in the browser. + +All lifecycle logic (start/stop, add/remove dataset, save/load state, โ€ฆ) is +inherited from :class:`~ansys.visor.viewer.app.visor_vtk.VisorVTK`. +This class only wires the LOCAL-specific scene and trame app. +""" + +from __future__ import annotations + +from ansys.visor.viewer.app.trame.local_app import LocalApp +from ansys.visor.viewer.app.visor_vtk import VisorVTK +from ansys.visor.viewer.config import settings +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger, get_trame_logger +from ansys.visor.viewer.vtk.scene.local_scene import VisorLocalScene + +logger = VisorDefaultLogger(__name__) + + +class VisorVTKLocal(VisorVTK): + """ + Visor interface for VTK LOCAL mode: browser-side rendering via VTK-WASM. + + All public API is inherited from :class:`VisorVTK`. Only the rendering + wiring (scene, trame app) is specific to this class. + + Use ``Visor(rendering_mode=RenderingMode.LOCAL)`` to obtain an instance. + """ + + def _initialize_rendering(self, standalone: bool, trame_log_dir: str | None) -> None: + """Wire up the LOCAL scene, and LocalApp.""" + dark_mode = self._dark_mode if self._dark_mode is not None else settings.default_dark_mode + + self._scene = VisorLocalScene(self.server, dark_mode=dark_mode) + + self._trame_app = LocalApp( + self.server, + lambda: self._scene.get_scene_details_json(), + lambda request_id, response: self._scene.handle_save_state_response(request_id, response), + standalone, + trame_logger=get_trame_logger(trame_log_dir), + pick_geometry=lambda + actor_wasm_id, + cell_id, + mode, + world_x, + world_y, + world_z: self._scene.pick_geometry(actor_wasm_id, cell_id, mode, world_x, world_y, world_z), + ) + diff --git a/src/ansys/visor/viewer/assets/Bunny.vtp b/src/ansys/visor/viewer/assets/Bunny.vtp new file mode 100644 index 00000000..e74d15cb --- /dev/null +++ b/src/ansys/visor/viewer/assets/Bunny.vtp @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="872" NumberOfVerts="502" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="1000" > + <PointData Normals="Normals"> + <DataArray type="Float32" Name="Normals" NumberOfComponents="3" format="appended" RangeMin="0.9999999605" RangeMax="1.0000000409" offset="0" /> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.063939122107" RangeMax="0.23461699359" offset="12384" /> + </Points> + <Verts> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="20624" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="21628" /> + </Verts> + <Lines> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="22628" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="22644" /> + </Lines> + <Strips> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="22660" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="22676" /> + </Strips> + <Polys> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="22692" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="29080" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _AQAAAACAAADgKAAANCQAAA==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AQAAAACAAADgKAAAEBgAAA==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AQAAAACAAADYBwAA3QIAAA==eJwV00OCIAYAALBZ27Zt27Zt27Zt27Zt27ZtVcv0kCckICAgIBCBCUJQghGcEIQkFKEJQ1jCEZ4IRCQSkYlCVKIRnRjEJBaxiUNc4hGfBCQkEYlJQlKSkZwUpCQVqUlDWtKRngxkJBOZyUJWspGdHOQkF7nJQ17ykZ8CFKQQhSlCUYpRnBKUpBSlKUNZylGeClSkEpWpQlWqUZ0a1KQWtalDXepRnwY0pBGNaUJTmtGcFrSkFa1pQ1va0Z4OdKQTnelCV7rRnR70pBe96UNf+tGfAQxkEIMZwlCGMZwRjGQUoxnDWMYxnglMZBKTmcJUpjGdGcxkFrOZw1zmMZ8FLGQRi1nCUpaxnBWsZBWrWcNa1rGeDWxkE5vZwla2sZ0d7GQXu9nDXvaxnwMc5BCHOcJRjnGcE5zkFKc5w1nOcZ4LXOQSl7nCVa5xnRvc5Ba3ucNd7nGfBzzkEY95wlOe8ZwXvOQVr3nDW97xng985BOf+cJXvvEXf/MP//If3/nBT37xm//zByIwQQhKMIITgpCEIjRhCEs4whOBiEQiMlGISjSiE4OYxCI2cYhLPOKTgIQkIjFJSEoykpOClKQiNWlISzrSk4GMZCIzWchKNrKTg5zkIjd5yEs+8lOAghSiMEUoSjGKU4KSlKI0ZShLOcpTgYpUojJVqEo1qlODmtSiNnWoSz3q04CGNKIxTWhKM5rTgpa0ojVtaEs72tOBjnSiM13oSje604Oe9KI3fehLP/ozgIEMYjBDGMowhjOCkYxiNGMYyzjGM4GJTGIyU5jKNKYzg5nMYjZzmMs85rOAhSxiMUtYyjKWs4KVrGI1a1jLOtazgY1sYjNb2Mo2trODnexiN3vYyz72c4CDHOIwRzjKMY5zgpOc4jRnOMs5znOBi1ziMle4yjWuc4Ob3OI2d7jLPe7zgIc84jFPeMoznvOCl7ziNW94yzve84GPfOIzX/jKN/4APrT2Lg==AQAAAACAAADYBwAA2wIAAA==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 + </AppendedData> +</VTKFile> diff --git a/src/ansys/visor/viewer/assets/carotid.vtk b/src/ansys/visor/viewer/assets/carotid.vtk new file mode 100644 index 00000000..a40b5e87 Binary files /dev/null and b/src/ansys/visor/viewer/assets/carotid.vtk differ diff --git a/src/ansys/visor/viewer/assets/horse.vtp b/src/ansys/visor/viewer/assets/horse.vtp new file mode 100644 index 00000000..9f6ad1bc --- /dev/null +++ b/src/ansys/visor/viewer/assets/horse.vtp @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" compressor="vtkZLibDataCompressor"> + <PolyData> + <Piece NumberOfPoints="33936" NumberOfVerts="0" NumberOfLines="0" NumberOfStrips="0" NumberOfPolys="67868" > + <PointData> + </PointData> + <CellData> + </CellData> + <Points> + <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="appended" RangeMin="0.0077540713456" RangeMax="0.11573082632" offset="0" /> + </Points> + <Verts> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="274088" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="274104" /> + </Verts> + <Lines> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="274120" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="274136" /> + </Lines> + <Strips> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="274152" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="274168" /> + </Strips> + <Polys> + <DataArray type="Int32" Name="connectivity" format="appended" RangeMin="" RangeMax="" offset="274184" /> + <DataArray type="Int32" Name="offsets" format="appended" RangeMin="" RangeMax="" offset="608196" /> + </Polys> + </Piece> + </PolyData> + <AppendedData encoding="base64"> + _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 + </AppendedData> +</VTKFile> diff --git a/src/ansys/visor/viewer/assets/plate.vtm b/src/ansys/visor/viewer/assets/plate.vtm new file mode 100644 index 00000000..43a525af --- /dev/null +++ b/src/ansys/visor/viewer/assets/plate.vtm @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<VTKFile type="vtkMultiBlockDataSet" version="1.0" byte_order="LittleEndian" header_type="UInt32" compressor="vtkZLibDataCompressor"> + <vtkMultiBlockDataSet> + <Block name="Block-00" index="0"> + <Block name="Block-00" index="0"> + <DataSet index="0" name="Block-00" file="plate/plate_0.vtp"/> + <DataSet index="1" name="Block-01" file="plate/plate_1.vtp"/> + <DataSet index="2" name="Block-02" file="plate/plate_2.vtp"/> + <DataSet index="3" name="Block-03" file="plate/plate_3.vtp"/> + <DataSet index="4" name="Block-04" file="plate/plate_4.vtp"/> + <DataSet index="5" name="Block-05" file="plate/plate_5.vtp"/> + </Block> + </Block> + </vtkMultiBlockDataSet> +</VTKFile> diff --git a/src/ansys/visor/viewer/cli/__init__.py b/src/ansys/visor/viewer/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/cli/apis.py b/src/ansys/visor/viewer/cli/apis.py new file mode 100644 index 00000000..029f1a99 --- /dev/null +++ b/src/ansys/visor/viewer/cli/apis.py @@ -0,0 +1,274 @@ +"""API client for controlling a Visor server.""" + +import json +import os +import time + +import requests +import uvicorn + +from ansys.visor.viewer.config import settings + + +class ServerAPI: + """Client for managing the Visor server process. + + Provides methods to start the server and interact with server-level + endpoints such as health checks, server info, initialization, and + listing available viewer instances. + + Parameters + ---------- + host : str + Hostname or IP address on which the server is (or will be) running. + port : int or str + Port number on which the server is (or will be) listening. + """ + def __init__(self, host, port): + """Initialize the server with viewer configuration.""" + self.host = host + self.port = port + binding_host = settings.binding_host or host + scheme = settings.url_scheme + self.base = f"{scheme}://{binding_host}:{port}" + + def start(self): + """Start the Visor server. + + Notes + ----- + This call blocks until the server is stopped. + """ + app = "ansys.visor.viewer.api.server:app" + + print( + f"Starting the Visor server using uvicorn.run({app!r}, host={self.host!r}, port={self.port!r})" + ) + + # Note: uvicorn.run() blocks until shutdown. + uvicorn.run(app, host=self.host, port=int(self.port), log_level="info") + + def health(self): + """Print the server health-check response.""" + url = f"{self.base}/health" + resp = requests.get(url) + print(resp.json()) + + def info(self): + """Print general information about the running server.""" + resp = requests.get(f"{self.base}/info") + print(resp.json()) + + def initialize(self, host, port, rendering_mode, standalone, dark_mode): + """Initialize the server with viewer configuration. + + Parameters + ---------- + host : str + Hostname the viewer client should connect to. + port : int + Port the viewer client should connect to. Pass ``0`` to let + the Visor server pick an unused port on its own host. + standalone : RenderingMode + Rendering mode to use for the viewer instance. Must be one of the + values defined in ``RenderingMode``. + standalone : bool + Whether to run in standalone mode (no external orchestrator). + dark_mode : bool + Whether to enable dark mode in the viewer UI. + """ + data = {"host": host, "port": port, "rendering_mode": rendering_mode, "standalone": standalone, "dark_mode": dark_mode} + resp = requests.post(f"{self.base}/initialize", json=data) + print(resp.json()) + + def list(self): + """Print the URLs of all currently available viewer instances.""" + resp = requests.get(f"{self.base}") + data = resp.json() + if 'urls' in data and data['urls']: + print("Available instances:") + for url in data['urls']: + print(" " + url) + + +class InstanceAPI: + """Client for controlling a single Visor viewer instance. + + Wraps the instance-level REST endpoints exposed by the Visor server, + allowing callers to start and stop visualizations, manage datasets, + and save or restore viewer state. + + Parameters + ---------- + host : str + Hostname or IP address of the Visor server. + port : int or str + Port number of the Visor server. + """ + def __init__(self, host, port): + binding_host = settings.binding_host or host + scheme = settings.url_scheme + self.base = f"{scheme}://{binding_host}:{port}" + + def start(self, file_path, metadata_path, timeout): + """Start a visualization for the given file. + + Parameters + ---------- + file_path : str + Path to the data file to visualize. + metadata_path : str + Path to the associated metadata file. + timeout : int or float + Maximum number of seconds to wait for the visualization to start. + """ + data = {"file_path": file_path, "metadata": metadata_path, "timeout": timeout} + resp = requests.post(f"{self.base}/start", json=data) + print(resp) + print(resp.json()) + + def update(self, file_path, metadata_path): + """Update the current visualization with new data. + + Parameters + ---------- + file_path : str + Path to the updated data file. + metadata_path : str + Path to the updated metadata file. + """ + data = {"file_path": file_path, "metadata": metadata_path} + resp = requests.post(f"{self.base}/update", json=data) + print(resp.json()) + + def add_dataset(self, file_path, metadata_path): + """Add an additional dataset to the current visualization. + + Parameters + ---------- + file_path : str + Path to the data file to add. + metadata_path : str + Path to the associated metadata file. + """ + data = {"file_path": file_path, "metadata": metadata_path} + resp = requests.post(f"{self.base}/add_dataset", json=data) + print(resp.json()) + + def list_datasets(self): + """Print all datasets currently loaded in the viewer.""" + resp = requests.get(f"{self.base}/list_datasets") + print(json.dumps(resp.json().get("datasets", {}), indent=4)) + + def remove_dataset(self, dataset_id: int): + """Remove a dataset from the current visualization. + + Parameters + ---------- + dataset_id : int + Numeric identifier of the dataset to remove. + """ + data = {"dataset_id": dataset_id} + resp = requests.post(f"{self.base}/remove_dataset", json=data) + print(resp.json()) + + def stop_visualization(self): + """Stop the active visualization without shutting down the instance.""" + resp = requests.post(f"{self.base}/stop_visualization") + print(resp.json()) + + def stop(self): + """Stop the viewer instance entirely.""" + resp = requests.post(f"{self.base}/stop") + print(resp.json()) + + def save(self, state_dir): + """Save the current viewer state to disk. + + Parameters + ---------- + state_dir : str + Directory path where the state files will be written. + """ + data = {"state_dir": state_dir} + resp = requests.post(f"{self.base}/save_state", json=data) + print(resp.json()) + + def load(self, state_dir): + """Load a previously saved viewer state from disk. + + Parameters + ---------- + state_dir : str + Directory path containing the state files to load. + """ + data = {"state_dir": state_dir} + resp = requests.post(f"{self.base}/load_state", json=data) + print(resp.json()) + + +class LogsAPI: + """Utility for browsing and tailing Visor log files. + + Provides methods to list available log files and print their contents, + with optional real-time following similar to ``tail -f``. + + Parameters + ---------- + log_dir : str, optional + Directory that contains the ``.log`` files. Defaults to + ``settings.default_log_dir`` when not supplied. + """ + def __init__(self, log_dir=None): + self.log_dir = log_dir or settings.default_log_dir + + def list_logs(self): + """Print the names of all ``.log`` files found in the log directory.""" + if not os.path.isdir(self.log_dir): + print(f"Log directory not found: {self.log_dir}") + return + files = [f for f in os.listdir(self.log_dir) if f.endswith(".log")] + if not files: + print("No log files found.") + else: + print("Available log files:") + for f in files: + print(" " + f[:-4]) # strip .log + + def show_log(self, log_name, follow=False, lines=10): + """Display the contents of a log file. + + Parameters + ---------- + log_name : str + Name of the log file to display, without the ``.log`` extension. + follow : bool, optional + When ``True``, print the last *lines* lines and then stream new + content as it is appended (like ``tail -f``). Defaults to + ``False``. + lines : int, optional + Number of lines from the end of the file to display. Defaults + to ``10``. + """ + log_path = os.path.join(self.log_dir, f"{log_name}.log") + try: + with open(log_path, "r") as f: + from collections import deque + if follow: + # Print last n lines first + f.seek(0) + last_lines = deque(f, maxlen=lines) + print("".join(last_lines), end="") + # Now follow new lines + f.seek(0, 2) + while True: + line = f.readline() + if not line: + time.sleep(0.5) + continue + print(line, end="") + else: + # Only print last n lines + print("".join(deque(f, maxlen=lines)), end="") + except FileNotFoundError: + print(f"Log file not found: {log_path}") diff --git a/src/ansys/visor/viewer/cli/parse_args.py b/src/ansys/visor/viewer/cli/parse_args.py new file mode 100644 index 00000000..e5151a6a --- /dev/null +++ b/src/ansys/visor/viewer/cli/parse_args.py @@ -0,0 +1,135 @@ +"""Argument parsing for visor-cli""" + +from argparse import ArgumentParser + +from ansys.visor.viewer.core.visor_enums import RenderingMode + + +def parse_args(): + """Parse the command line arguments.""" + parser = ArgumentParser("visor-cli") + + parser.add_argument( + "--api-host", + default="localhost", + help="API host (default: localhost)" + ) + parser.add_argument( + "--api-port", + default=53211, + type=int, + help="API port (default: 53211)") + + subparsers = parser.add_subparsers(dest="group", required=True) + + + ##################### + # Server subcommands + ##################### + server_parser = subparsers.add_parser("server", help="Server operations") + server_sub = server_parser.add_subparsers(dest="action", required=True) + + # start service + server_sub.add_parser("start", help="Start the API server (runs uvicorn command)") + + # health API + server_sub.add_parser("health", help="Check server health") + + # info API + server_sub.add_parser("info", help="Print server info") + + # initialize API + init_parser = server_sub.add_parser("init", help="Initialize a Visor instance") + init_parser.add_argument("--host", default="localhost", + help="Instance host (default: localhost)") + init_parser.add_argument("--port", type=int,help="Instance port (default: 0, meaning the app chooses an unused port)", default=0) + init_parser.add_argument( + "--rendering-mode", + choices=[e.name for e in RenderingMode], + default=None, + help=( + "Rendering mode to use. Choices: " + + ", ".join(e.name for e in RenderingMode) + + ". Default: LOCAL. Use HEADLESS for headless/no-GPU mode." + ), + ) + init_parser.add_argument( + "--standalone", + type=lambda x: x.lower() == "true", + default=None, + help="Standalone mode (True or False). Cannot be changed on an existing instance." + ) + init_parser.add_argument( + "--dark-mode", + type=lambda x: x.lower() == "true", + default=None, + help="Dark mode (True or False, default: from settings or server default)" + ) + + # list API + server_sub.add_parser("list", help="List Visor instances") + + + ###################### + # Instance subcommands + ###################### + instance_parser = subparsers.add_parser("instance", help="Instance operations") + instance_sub = instance_parser.add_subparsers(dest="action", required=True) + + # start API + start_parser = instance_sub.add_parser("start", help="Start a visualization") + start_parser.add_argument("file_path", nargs="?", default=None, help="File to load in viewer") + start_parser.add_argument("--metadata-path", type=str, default=None, help="Path to metadata JSON file") + start_parser.add_argument("--timeout", type=int, default=0, help="Timeout in seconds") + + # update API + update_parser = instance_sub.add_parser("update", help="Update with new input") + update_parser.add_argument("file_path", help="File to update in viewer") + update_parser.add_argument("--metadata-path", type=str, default=None, help="Path to metadata JSON file") + + # add dataset API + add_dataset_parser = instance_sub.add_parser("add", help="Add new dataset to instance") + add_dataset_parser.add_argument("file_path", help="File to update in viewer") + add_dataset_parser.add_argument("--metadata-path", type=str, default=None, help="Path to metadata JSON file") + + # list datasets API + instance_sub.add_parser("list", help="List datasets loaded in instance") + + # remove dataset API + remove_dataset_parser = instance_sub.add_parser("remove", help="Remove dataset from instance") + remove_dataset_parser.add_argument("dataset_id", type=int, help="ID of datasets to remove") + + # stop_visualization API + instance_sub.add_parser("stop_visualization", help="Stop visualization") + + # stop API + instance_sub.add_parser("stop", help="Stop and clean up the instance") + + # save state API + save_state_sub = instance_sub.add_parser("save", help="Save the current state of the instance") + save_state_sub.add_argument("state_dir", help="Directory to save the instance state") + + # load state API + load_state_sub = instance_sub.add_parser("load", help="Load the current state of the instance") + load_state_sub.add_argument("state_dir", help="Directory to load the instance state from") + + + ###################### + # Logs subcommands + ###################### + logs_parser = subparsers.add_parser("logs", help="Log file operations") + logs_parser.add_argument("log_name", nargs="?", help="Name of the log file (without .log)") + logs_parser.add_argument("-f", "--follow", + action="store_true", + help="Follow the log file (like tail -f)" + ) + logs_parser.add_argument("--log-dir", + default=None, + help="Directory containing log files (default: from settings)" + ) + logs_parser.add_argument( + "-n", "--lines", type=int, default=10, + help="Number of lines to show from the end of the log file (default: 10)" + ) + + return parser.parse_args() diff --git a/src/ansys/visor/viewer/cli/visor_cli.py b/src/ansys/visor/viewer/cli/visor_cli.py new file mode 100644 index 00000000..1741b55f --- /dev/null +++ b/src/ansys/visor/viewer/cli/visor_cli.py @@ -0,0 +1,146 @@ +"""Command line interface for Visor Viewer.""" + +import sys + +import requests + +from ansys.visor.viewer.cli.apis import ( + InstanceAPI, + LogsAPI, + ServerAPI, +) +from ansys.visor.viewer.cli.parse_args import parse_args +from ansys.visor.viewer.config import settings + + +def check_server_running(host, port): + """Check if the server is running by making a health check request""" + try: + binding_host = settings.binding_host or host + scheme = settings.url_scheme + resp = requests.get(f"{scheme}://{binding_host}:{port}/health", timeout=1) + if resp.status_code == 200: + return True + print("Server health check failed:", resp.text) + except Exception as e: + print("Could not connect to server:", e) + return False + +def check_init_args( + api_host, + api_port, + host, + port, + rendering_mode, + standalone, + dark_mode +): + """ + If the user explicitly passed --rendering-mode, --standalone, or --dark-mode, check whether + the target instance already exists. These flags have no effect when + connecting to an existing instance, so we treat it as an error to + prevent silent misconfiguration. + """ + # Check if --rendering-mode, --standalone, or --dark-mode args are passed + user_set_flags = [] + if rendering_mode is not None: + user_set_flags.append("--rendering-mode") + if standalone is not None: + user_set_flags.append("--standalone") + if dark_mode is not None: + user_set_flags.append("--dark-mode") + + # If neither is passed, no need to check for existing instance since we're not trying to set any params + if not user_set_flags: + return + + # If --standalone or --dark_mode args are passed, check if target instance already exists on server + binding_host = settings.binding_host or host + scheme = settings.url_scheme + target_url = f"{scheme}://{binding_host}:{port}" + try: + api_scheme = settings.url_scheme + existing = requests.get( + f"{api_scheme}://{api_host}:{api_port}/", timeout=5 + ).json().get("urls", []) + except Exception as e: + print(f"Warning: could not check existing instances: {e}") + existing = [] + + if target_url in existing: + print( + f"\nError: Instance already initialized on {target_url}.\n" + f"Cannot use {', '.join(user_set_flags)} when connecting to an existing instance. " + f"{'These values are' if len(user_set_flags) > 1 else 'This value is'}" + " fixed at creation time and cannot be changed.\n" + ) + sys.exit(1) + + + +def main(): + """Main CLI tool for Visor Viewer.""" + args = parse_args() + + # Check if the server is running before executing instance commands + # unless the command is to start the server + if not (args.group == "server" and args.action in ["start", "health"]): + if not check_server_running(args.api_host, args.api_port): + print("Server is not running. Please start the server first.") + sys.exit(1) + + # Execute the appropriate API action based on the group and action + if args.group == "server": + api = ServerAPI(args.api_host, args.api_port) + if args.action == "start": + api.start() + elif args.action == "health": + api.health() + elif args.action == "info": + api.info() + elif args.action == "init": + check_init_args( + args.api_host, + args.api_port, + args.host, + args.port, + args.rendering_mode, + args.standalone, + args.dark_mode + ) + api.initialize(args.host, args.port, args.rendering_mode, args.standalone, args.dark_mode) + elif args.action == "list": + api.list() + elif args.group == "instance": + api = InstanceAPI(args.api_host, args.api_port) + if args.action == "start": + api.start(args.file_path, args.metadata_path, args.timeout) + elif args.action == "update": + api.update(args.file_path, args.metadata_path) + elif args.action == "add": + api.add_dataset(args.file_path, args.metadata_path) + elif args.action == "remove": + api.remove_dataset(args.dataset_id) + elif args.action == "list": + api.list_datasets() + elif args.action == "stop_visualization": + api.stop_visualization() + elif args.action == "stop": + api.stop() + elif args.action == "save": + api.save(args.state_dir) + elif args.action == "load": + api.load(args.state_dir) + + if args.group == "logs": + api = LogsAPI(log_dir=args.log_dir) + if not args.log_name: + api.list_logs() + else: + api.show_log(args.log_name, args.follow, args.lines) + return + + + +if __name__ == "__main__": + main() diff --git a/src/ansys/visor/viewer/config.py b/src/ansys/visor/viewer/config.py new file mode 100644 index 00000000..ff474d5c --- /dev/null +++ b/src/ansys/visor/viewer/config.py @@ -0,0 +1,117 @@ +"""Settings for Visor app.""" + +import os +from pathlib import Path + +import yaml +from pydantic import Field, model_validator +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """ + Settings for Visor app. + """ + app_name: str = "Visor Viewer" + default_host: str = "localhost" + default_port: int = 8081 + default_standalone: bool = True + default_dark_mode: bool = False + default_client_bundle: str = str(Path(__file__).parent.joinpath("client_bundle")) + # Log directory resolution order (first match wins): + # 1. .visor config file โ€” "default_log_dir: /some/path" + # 2. GLOW_PROJECT_FILES_DIRECTORY env var โ€” resolves to <GLOW_DIR>/../logs + # 3. Startup directory fallback โ€” <cwd>/logs + default_log_dir: str = Field( + default_factory=lambda: ( + str(Path(os.environ["GLOW_PROJECT_FILES_DIRECTORY"]).parent / "logs") + if os.environ.get("GLOW_PROJECT_FILES_DIRECTORY") + else str(Path.cwd() / "logs") + ) + ) + trame_log_dir: str | None = None + perf_logging: bool = False # enable via .visor YAML or by passing Settings(perf_logging=True) + # Optional externally routed binding host. Read from env once at startup + # if present; otherwise leave as None to allow per-instance host fallback. + binding_host: str | None = Field(default_factory=lambda: os.environ.get("GLOW_PRODUCT_BINDING_HOST")) + # Optional SSL certificate for Trame/wslink in the format "<cert>,<key>". + ssl_certificate: str | None = None + + @classmethod + def settings_customise_sources( + cls, + settings_cls, + init_settings, + env_settings, + file_secret_settings, + **kwargs + ): + """Customise the settings for Visor app.""" + def visor_file_settings(): + """Read .visor file settings.""" + config_path = Path.cwd() / ".visor" + if config_path.exists(): + print(f'Loading Visor settings from {config_path}') + with open(config_path, "r") as f: + data = yaml.safe_load(f) or {} + for key in ["app_name", "default_client_bundle"]: + data.pop(key, None) + return data + return {} + return (visor_file_settings, init_settings) + + def get_client_bundle(self, standalone: bool = True) -> str | None: + """Get the client bundle path based on the standalone flag.""" + if standalone: + return self.default_client_bundle + return None + + @model_validator(mode="after") + def _resolve_ssl_certificate(self) -> "Settings": + # Normalize empty or whitespace-only strings to None so consumers + # can rely on ``ssl_certificate`` being either a non-empty string + # or None. This also avoids truthy non-string values (e.g. MagicMock) + # from being interpreted as valid certificates. + if isinstance(self.ssl_certificate, str): + if not self.ssl_certificate.strip(): + self.ssl_certificate = None + else: + return self + # If a non-string (e.g. MagicMock) was set, coerce to None + if self.ssl_certificate is not None and not isinstance(self.ssl_certificate, str): + self.ssl_certificate = None + + # Resolve from environment variables if present + cert = self._get_ssl_certificate() + if cert: + self.ssl_certificate = cert + return self + + @property + def url_scheme(self) -> str: + """Return the URL scheme to advertise: 'https' when a non-empty + SSL certificate is configured, otherwise 'http'. + + Use this property throughout the codebase instead of duplicating + the ssl_presence check. + """ + ssl_val = self.ssl_certificate + return "https" if isinstance(ssl_val, str) and ssl_val.strip() else "http" + + @staticmethod + def _get_ssl_certificate() -> str | None: + certs_dir = os.getenv("GLOW_CERTS_DIR") or os.getenv("ANSYS_GRPC_CERTIFICATES") + if not certs_dir: + return None + p = Path(certs_dir) + if not p.exists() or not p.is_dir(): + return None + candidates = [("server.crt", "server.key"), ("client.crt", "client.key")] + for cert_name, key_name in candidates: + cert_path = p / cert_name + key_path = p / key_name + if cert_path.exists() and key_path.exists(): + return f"{str(cert_path)},{str(key_path)}" + return None + +settings = Settings() diff --git a/src/ansys/visor/viewer/core/__init__.py b/src/ansys/visor/viewer/core/__init__.py new file mode 100644 index 00000000..db0bbd89 --- /dev/null +++ b/src/ansys/visor/viewer/core/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2025 ANSYS, Inc. All Rights Reserved. +# Restricted Rights Legend: See LICENSE for details. diff --git a/src/ansys/visor/viewer/core/errors.py b/src/ansys/visor/viewer/core/errors.py new file mode 100644 index 00000000..de2fe298 --- /dev/null +++ b/src/ansys/visor/viewer/core/errors.py @@ -0,0 +1,61 @@ +"""Custom defined exception for Visor viewer application.""" + + +class InvalidFileError(Exception): + """Raised when an invalid file is provided""" + def __init__(self,value): + self.value = value + def __str__(self): + return repr(f"Invalid file: {self.value}") + +class InvalidServerTimeoutError(Exception): + """Raised when an invalid timeout value is provided""" + def __init__(self,value): + self.value = value + def __str__(self): + return repr(f"Invalid timeout value: {self.value}") + +class InvalidUrlError(Exception): + """Raised when an invalid url is provided""" + def __init__(self,value): + self.value = value + def __str__(self): + return repr(f"Invalid url: {self.value}") + +class InvalidPathError(Exception): + """Raised when an invalid path is provided""" + def __init__(self,value): + self.value = value + def __str__(self): + return repr(f"Invalid path: {self.value}") + +class InvalidFileFormatError(Exception): + """Raised when an invalid file format is provided""" + def __init__(self,value): + self.value = value + def __str__(self): + return repr(f"Invalid file format: {self.value}") + +class ServerNotStartedError(Exception): + """Raised when the server is not started""" + def __init__(self,value): + self.value = value + def __str__(self): + return repr(f"Server not started: {self.value}") + +class VisorAddDatasetError(Exception): + """Raised when adding/loading a dataset into a Visor scene fails.""" + + def __init__(self, message: str, *, input_path: str | None = None, metadata_path: str | None = None): + super().__init__(message) + self.input_path = input_path + self.metadata_path = metadata_path + + def __str__(self): + base = super().__str__() + details = [] + if self.input_path: + details.append(f"input_path={self.input_path}") + if self.metadata_path: + details.append(f"metadata_path={self.metadata_path}") + return base if not details else f"{base} ({', '.join(details)})" diff --git a/src/ansys/visor/viewer/core/metadata.py b/src/ansys/visor/viewer/core/metadata.py new file mode 100644 index 00000000..7e6c97f4 --- /dev/null +++ b/src/ansys/visor/viewer/core/metadata.py @@ -0,0 +1,25 @@ +"""Metadata model for providing information about the dataset to the visualizer.""" + +from pydantic import BaseModel, Field, ValidationError, model_validator + +from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState + + +class Metadata(BaseModel): + name: str + unit: str + state: PersistedDatasetState = Field(default_factory=dict) + + @model_validator(mode="after") + def ensure_state(self) -> "Metadata": + if not isinstance(self.state, PersistedDatasetState): + try: + self.state = PersistedDatasetState.model_validate(self.state) + except ValidationError: + raise + return self + + +class ExtendedMetadata(Metadata): + file_path: str | None = None + metadata_path: str | None = None diff --git a/src/ansys/visor/viewer/core/net.py b/src/ansys/visor/viewer/core/net.py new file mode 100644 index 00000000..63af3414 --- /dev/null +++ b/src/ansys/visor/viewer/core/net.py @@ -0,0 +1,69 @@ +"""Networking utilities for Visor (port selection, admin-privilege detection).""" + +import os +import socket + + +def is_user_admin() -> bool: + """ + Check to see if the current user is likely to have root/administrator level system + access. Under Windows, this is not a complete test, but it is a reasonable proxy. + + :returns bool: True if the current user has higher-level access permissions + """ + try: + # on Windows this will throw AttributeError + return os.geteuid() + except AttributeError: + try: + import ctypes + + # on non-Windows systems, this can be ModuleNotFoundError + # on some Windows machines this can be AttributeError + return ctypes.windll.shell32.IsUserAnAdmin() == 1 + except (ModuleNotFoundError, AttributeError): + return False + + +def find_unused_port( + host: str = "", + avoid: list[int] | None = None, +) -> int | None: + """ + Find a single unused port on the given host by asking the OS to allocate + a free port from the ephemeral range. + + The probe binds on ``host`` (empty string ``""`` means all interfaces, + equivalent to ``0.0.0.0``), so a port considered free here is free on + the exact interface the caller intends to bind to. This matters in + multi-host / multi-interface setups where a port free on loopback may + still be in use on an external interface. + + Non-admin users cannot bind to privileged ports [1-1023], so if such a + port is somehow returned it is rejected. + + :param str host: hostname or IP to probe on (``""`` for all interfaces) + :param Optional[List[int]] avoid: an optional list of ports not to return + :returns Optional[int]: the detected port or None on failure + """ + if avoid is None: + avoid = [] + # Avoid handing back privileged ports to a non-privileged user + is_admin = is_user_admin() + # Try a bounded number of times in case the OS repeatedly hands us a + # port that is in the "avoid" list or a privileged port. + for _ in range(1000): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.bind((host, 0)) + port = sock.getsockname()[1] + finally: + sock.close() + # skip privileged ports for non-admin users + if not is_admin and port < 1024: + continue + if port in avoid: + continue + return port + # in case we failed... + return None diff --git a/src/ansys/visor/viewer/core/perf_timer.py b/src/ansys/visor/viewer/core/perf_timer.py new file mode 100644 index 00000000..79b975e7 --- /dev/null +++ b/src/ansys/visor/viewer/core/perf_timer.py @@ -0,0 +1,167 @@ +""" +perf_timer.py +------------- +Lightweight multi-phase wall-clock timer that emits a single ``[PERF]`` +log line (compatible with PerfLogReader) only when **both** of the +following conditions are true: + +1. ``perf_logging`` is enabled via the application settings + (``settings.perf_logging``, set through the ``.visor`` config file) โ€” + the global on/off switch. +2. The ``PerfTimer`` was constructed with ``enabled=True`` (the default) โ€” + a per-call-site switch that lets you explicitly opt in only the APIs + that have been fully instrumented, while still passing a no-op timer + into shared helpers (e.g. ``_prepare_and_render_scene``) from + un-instrumented call paths. + +Usage +----- + from ansys.visor.viewer.core.perf_timer import PerfTimer + + # Fully instrumented API โ€” will record phases when perf_logging is on. + timer = PerfTimer("VisorDataset.update_variables", logger, n_vars=3) + + with timer.phase("validate"): + ... + with timer.phase("vtk_set_loop"): + ... + with timer.phase("reload"): + ... + + timer.log() + # -> [PERF] VisorDataset.update_variables n_vars=3 + # | validate=0.0012s | vtk_set_loop=0.0340s | reload=0.0008s + + # Shared helper that accepts an optional timer. Un-instrumented callers + # simply omit the argument; the helper creates a disabled no-op internally. + def _prepare_and_render_scene(self, input, metadata, timer: PerfTimer = None): + if timer is None: + timer = PerfTimer.disabled() + with timer.phase("finalize_scene"): + ... + +The ``[PERF]`` line format intentionally matches the regex patterns in +``ansys.visor.viewer.utils.perf_log_reader`` so that PerfLogReader can +parse reports without any changes. + +When either gate is off, both ``timer.phase()`` and ``timer.log()`` are +no-ops with negligible overhead. +""" +from __future__ import annotations + +import time +from contextlib import contextmanager +from typing import Iterator + +from ansys.visor.viewer.config import settings + + +class PerfTimer: + """ + Multi-phase wall-clock timer that produces a single ``[PERF]`` log line. + + Parameters + ---------- + label: + Text placed immediately after ``[PERF]``. + E.g. ``"VisorDataset.update_variables"`` or + ``"_update_variable 'pressure'"`` + logger: + Any logger with an ``info(str)`` method (typically a + ``VisorDefaultLogger`` instance). + enabled: + Per-call-site opt-in flag. Set to ``False`` for APIs that have not + yet been instrumented but still need to pass a no-op timer into a + shared helper. Defaults to ``True`` so that existing instrumented + call sites are unaffected. Recording only happens when *both* this + flag and ``settings.perf_logging`` are ``True``. + **extra_fields: + Key/value pairs appended to the label segment before the phase + timings, e.g. ``n_vars=3`` or ``n_tuples=1024``. + + Notes + ----- + ``settings.perf_logging`` is evaluated when each ``PerfTimer`` is + constructed (i.e. on each call-site invocation), not at module import + time. This means a ``.visor`` config change takes effect on the next + server start without any code changes. + """ + + __slots__ = ("_label", "_logger", "_extra", "_phases", "_enabled") + + @classmethod + def disabled(cls) -> "PerfTimer": + """ + Return a no-op timer. + + Used inside shared helpers that accept an optional ``timer`` argument + to normalise ``None`` into a safe no-op, so every downstream + ``timer.phase()`` call can be written unconditionally:: + + def _prepare_and_render_scene(self, input, metadata, + timer: PerfTimer = None): + if timer is None: + timer = PerfTimer.disabled() + with timer.phase("finalize_scene"): + ... + + Un-instrumented callers simply omit ``timer=`` โ€” they do **not** need + to construct ``PerfTimer.disabled()`` themselves. The returned + instance never records phases or emits log lines regardless of the + global ``settings.perf_logging`` flag. + """ + return cls("", logger=None, enabled=False) + + def __init__(self, label: str, logger=None, *, enabled: bool = True, **extra_fields): + """Initialize the timer.""" + self._label = label + self._logger = logger + self._extra: dict = extra_fields + self._phases: list[tuple[str, float]] = [] + # Both gates must be open: the global config flag AND the explicit + # per-call-site opt-in. + self._enabled: bool = enabled and settings.perf_logging + + @contextmanager + def phase(self, name: str) -> Iterator[None]: + """ + Context manager that times the enclosed block and records it as a + named phase. Is a no-op when perf logging is disabled. + """ + if not self._enabled: + yield + return + t0 = time.perf_counter() + yield + self._phases.append((name, time.perf_counter() - t0)) + + def log(self, *, include_total: bool = False) -> None: + """ + Emit the accumulated ``[PERF]`` line to the logger. + Does nothing when perf logging is disabled. + + Parameters + ---------- + include_total: + When ``True``, appends a ``total=Xs`` field that is the sum of + all recorded phases. Useful when the caller wants a single + top-level summary field (e.g. ``update_variables`` in the + trame interface). + """ + if not self._enabled or self._logger is None: + return + + # Build the label segment: "[PERF] <label> key=val key=val ..." + label_part = f"[PERF] {self._label}" + for k, v in self._extra.items(): + label_part += f" {k}={v}" + + # Build phase segments: "phase_name=0.0000s" + phase_parts = [f"{name}={elapsed:.4f}s" for name, elapsed in self._phases] + + if include_total: + total = sum(elapsed for _, elapsed in self._phases) + phase_parts.append(f"total={total:.4f}s") + + self._logger.info(" | ".join([label_part] + phase_parts)) + diff --git a/src/ansys/visor/viewer/core/visor_colors.py b/src/ansys/visor/viewer/core/visor_colors.py new file mode 100644 index 00000000..2823061a --- /dev/null +++ b/src/ansys/visor/viewer/core/visor_colors.py @@ -0,0 +1,9 @@ +"""Class defining colors used in Visor""" + +class VisorColors: + """Default colors used in Visor Viewer.""" + BackgroundColor: tuple[float, float, float] = (0,0,0) # <-- keep this at 0,0,0 to allow for color changes on the frontend with CSS + DefaultMeshColor: tuple[float, float, float] = (0.8, 0.8, 0.8) + DefaultSelectionColor: tuple[float, float, float] = (0.184, 0.427, 0.620) + DefaultSelectedMeshEdgeColor: tuple[float, float, float] = (0, 0, 0) + diff --git a/src/ansys/visor/viewer/core/visor_enums.py b/src/ansys/visor/viewer/core/visor_enums.py new file mode 100644 index 00000000..dea392a9 --- /dev/null +++ b/src/ansys/visor/viewer/core/visor_enums.py @@ -0,0 +1,84 @@ +"""Enums used in the Visor visualizer application..""" + + +from enum import Enum +from typing import Union + + +class RenderingEngine(Enum): + """ + An enumeration representing different rendering engines. + + Attributes: + VTK (int): Represents the VTK rendering engine. + """ + + VTK = 0 + + @classmethod + def from_string(cls, value: Union[str, "RenderingEngine"]) -> "RenderingEngine": + """ + Accept an enum member or a case-insensitive string name. + + Examples + -------- + >>> RenderingEngine.from_string("vtk") + <RenderingEngine.VTK: 'vtk'> + >>> RenderingEngine.from_string("VTK") + <RenderingEngine.VTK: 'vtk'> + """ + if isinstance(value, cls): + return value + if isinstance(value, str): + try: + return cls[value.upper()] + except KeyError: + pass + valid = [e.name for e in cls] + raise ValueError(f"Invalid RenderingEngine {value!r}. Valid values: {valid}") + + +class RenderingMode(Enum): + """ + How the chosen rendering engine is deployed. + + Attributes: + LOCAL: Client-side rendering via VTK-WASM in the browser. + """ + LOCAL = "local" + + @classmethod + def from_string(cls, value: Union[str, "RenderingMode"]) -> "RenderingMode": + """ + Accept an enum member or a case-insensitive string name. + + Examples + -------- + >>> RenderingMode.from_string("local") + <RenderingMode.LOCAL: 'local'> + """ + if isinstance(value, cls): + return value + if isinstance(value, str): + try: + return cls[value.upper()] + except KeyError: + pass + valid = [e.name for e in cls] + raise ValueError(f"Invalid RenderingMode {value!r}. Valid values: {valid}") + + +class VisorNodeType(Enum): + """ + An enumeration of the different types of nodes in the Visor scene graph. + """ + ROOT = "root" + GROUP = "group" + PART = "part" + +class VisorVtkVariableType(Enum): + """ + An enumeration of the different types of VTK variables. + """ + POINT = "POINT" + CELL = "CELL" diff --git a/src/ansys/visor/viewer/core/visor_helpers.py b/src/ansys/visor/viewer/core/visor_helpers.py new file mode 100644 index 00000000..dd5268e6 --- /dev/null +++ b/src/ansys/visor/viewer/core/visor_helpers.py @@ -0,0 +1,71 @@ +"""Helper functions for Visor Viewer.""" + +import random +import re + +from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet, vtkMultiPieceDataSet, vtkPolyData, vtkUnstructuredGrid + +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +logger = VisorDefaultLogger(__name__) + + +def get_random_javascript_safe_id(): + """ + Returns a random integer between 1000000000000000 and 9007199254740991 (inclusive) + that does not exceed JavaScript's Number.MAX_SAFE_INTEGER and is exactly 16 digits long. + In JavaScript, double precision floating point format only has 52 bits to represent + the mantissa, so it can only safely represent integers between -(253 โ€“ 1) and 253 โ€“ 1. + This function is useful for cases where IDs need to be handed over to a JavaScript + environment. Exceeding JavaScript's maximum integer will cause false positives + when equating integers, for example, when searching an array of IDs in search of a + specific ID. + """ + return random.randint(1000000000000000, 9007199254740991) + +def validate_url(url: str): + """URL validation""" + + if url is None or not isinstance(url, str): + return None + + regex = re.compile( + r"^(?:http)?://" # http:// or https:// + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain... + r"localhost|" # localhost... + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip + r"(?::\d+)?" # optional port + r"(?:/?|[/?]\S+)$", + re.IGNORECASE, + ) + + return re.match(regex, url) + +def validate_host(host: str) -> bool: + """ + Validates a host string (domain, IP, or localhost) without protocol or port. + Returns True if valid, False otherwise. + """ + if host is None or not isinstance(host, str): + return False + + regex = re.compile( + r"^(" # start group + r"(localhost)" # localhost + r"|" + r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # IPv4 + r"|" + r"([A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?(?:\.[A-Z0-9-]{1,63})+)" # domain + r")$", re.IGNORECASE + ) + return re.match(regex, host) is not None + +def is_composite_dataset(dataset) -> bool | None: + """ Determine the node type based on the dataset type.""" + if isinstance(dataset, (vtkMultiBlockDataSet, vtkMultiPieceDataSet)): + return True + if isinstance(dataset, (vtkUnstructuredGrid, vtkPolyData)): + return False + msg = f"node type not yet supported: {type(dataset)}" + logger.error(msg) + raise RuntimeError(msg) diff --git a/src/ansys/visor/viewer/core/visor_logging.py b/src/ansys/visor/viewer/core/visor_logging.py new file mode 100644 index 00000000..34b4df62 --- /dev/null +++ b/src/ansys/visor/viewer/core/visor_logging.py @@ -0,0 +1,173 @@ +"""Logging configuration for Visor""" + +import logging +from logging import Logger +from pathlib import Path + +from ansys.visor.viewer.config import settings + + +class VisorLogger(Logger): + """ + Custom logger for the Visor app. + + This logger writes logs to a file, allows setting the log level, + and ensures the log directory exists. + + Args: + name (str): The name of the logger, typically the module or class name. + filename (str): The name of the log file. + log_dir (Optional[str]): Directory to save logs. + Default is the default_log_dir from config settings. + level (int): Logging level. Default is logging.DEBUG. + """ + + # Logging format to comply with Ansys ADR: + # https://github.com/ansys-internal/architecture-decision-records/blob/main/content/ + # docs/adrs/0016-observability-strategy.md + LOGGING_FORMAT = ( + "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d %(funcName)s()] - %(message)s" + ) + ENCODING = "utf-8" + + def __init__(self, + name: str, + filename: str, + log_dir: str | None = None, + level: int = logging.DEBUG, + basic_config: bool = True, + propagate: bool = False): + """Initialize the logger.""" + # Initialize the parent class + super().__init__(name, level) + + # Log level + self.level = level + + # Set up the log directory and file path + self.filename = filename + self.log_dir = log_dir + if log_dir is None: + self.log_dir = settings.default_log_dir + self.file_path = self.get_file_path() + + # Ensure the log directory exists + self.create_dir() + + if basic_config: + # Set up basic config unless set to False + self.__basic_config() + + # Create file handler and set logging level + file_handler = logging.FileHandler( + self.file_path, + encoding=self.ENCODING + ) + file_handler.setLevel(level) + + # Add formatter for file handler + formatter = logging.Formatter(self.LOGGING_FORMAT) + file_handler.setFormatter(formatter) + + self.addHandler(file_handler) + + # Prevent propagation to the root logger + self.propagate = propagate + + def __basic_config(self): + """ + Configure the root logger via basicConfig + (this will affect any logger that doesn't have a handler) + """ + logging.basicConfig( + level=self.level, + format=self.LOGGING_FORMAT, + handlers=[ + logging.FileHandler( + self.file_path, + encoding=self.ENCODING + ) + ], + ) + + def create_dir(self) -> None: + Path(self.log_dir).mkdir(parents=True, exist_ok=True) + + def get_file_path(self) -> Path: + return Path(self.log_dir).joinpath(self.filename) + + +class VisorDefaultLogger(VisorLogger): + """ + Custom logger for Visor app with a default log file. + + This logger writes to a fixed log file named "visor.log", ensuring + consistency across multiple modules within the project. + It inherits from the VisorLogger class, which allows for centralized + configuration and logging. + + This default logger is intended for logging all of the project-related + messages to the same log file across different modules while maintaining + a consistent logging format and level. + + Args: + name (str): The name of the logger, typically the module or class name. + """ + def __init__(self, name): + # Initialize the parent class + super().__init__(name, "visor.log") + + +def set_up_named_logger(log_name, log_filename, log_dir, level=logging.DEBUG): + """Configure the logger for the named logger.""" + Path(log_dir).mkdir(parents=True, exist_ok=True) + + root_logger = logging.getLogger(log_name) + root_logger.setLevel(level) + + # Create file handler and set logging level + file_handler = logging.FileHandler(Path(log_dir).joinpath(log_filename)) + file_handler.setLevel(level) + + # Add formatter for file handler + formatter = logging.Formatter(VisorLogger.LOGGING_FORMAT) + file_handler.setFormatter(formatter) + + root_logger.addHandler(file_handler) + return root_logger + + +def configure_trame_logging(trame_log_dir: str) -> VisorLogger: + """Configure the loggers for the trame application""" + trame_log_filename = "visor_trame_app.log" + + # Set up custom logger for writing lifecycle events + lifecycle_logger = VisorLogger( + "trame_lifecycle", + trame_log_filename, + trame_log_dir, + level=logging.DEBUG, + basic_config=False, + propagate=True # Keep the logging in the default log file + ) + + # trame's internal python loggers + # set up file handlers to write to visor_trame_app.log + set_up_named_logger("trame", trame_log_filename, trame_log_dir) + set_up_named_logger("trame_server", trame_log_filename, trame_log_dir) + set_up_named_logger("trame.app", trame_log_filename, trame_log_dir) + + return lifecycle_logger + +def get_trame_logger(trame_log_dir: str | None) -> VisorLogger | None: + """ For trame Python logging, output to app log file """ + if trame_log_dir: + return configure_trame_logging(trame_log_dir) + return None + +def get_trame_log_path(trame_log_dir: str | None) -> Path | bool: + """For Server option to enable network logging, set output file path""" + if trame_log_dir: + return Path(trame_log_dir).joinpath("visor_trame_network.log") + return False + diff --git a/src/ansys/visor/viewer/core/visor_types.py b/src/ansys/visor/viewer/core/visor_types.py new file mode 100644 index 00000000..f5771637 --- /dev/null +++ b/src/ansys/visor/viewer/core/visor_types.py @@ -0,0 +1,23 @@ +"""Aliases for Visor types.""" + + +from typing import Sequence, TypeAlias, Union + +import numpy as np +from vtkmodules.vtkCommonDataModel import ( + vtkDataObject, + vtkMultiBlockDataSet, + vtkMultiPieceDataSet, + vtkPolyData, + vtkUnstructuredGrid, +) + +VisorDatasetType: TypeAlias = ( + vtkDataObject + | vtkMultiBlockDataSet + | vtkMultiPieceDataSet + | vtkUnstructuredGrid + | vtkPolyData +) + +ArrayLike = Union[np.ndarray, Sequence[float]] diff --git a/src/ansys/visor/viewer/models/__init__.py b/src/ansys/visor/viewer/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/common/__init__.py b/src/ansys/visor/viewer/models/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/common/part_properties.py b/src/ansys/visor/viewer/models/common/part_properties.py new file mode 100644 index 00000000..ff20cfcc --- /dev/null +++ b/src/ansys/visor/viewer/models/common/part_properties.py @@ -0,0 +1,26 @@ +"""Model for properties on a dataset part in the Visor visualization.""" + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class PartProperties(BaseModel): + """ + Represents properties for a part in the Visor visualization. + + opacity: The opacity of the part, between 0.0 (fully transparent) and 1.0 (fully opaque). + visible: Whether the part is visible in the scene. + selected: Whether the part is currently selected by the user. + color_by: If set, this part's color is determined by the values of the variable with this ID. + color_by_component: If the variable specified by color_by has multiple components, + this specifies which component to use for coloring. + diffuse_rgb: If set, this part's color is determined by the specified RGB values (each between 0 and 1). + + """ + opacity: Optional[float] = Field(default=None, ge=0.0, le=1.0) + visible: Optional[bool] = Field(default=None) + selected: Optional[bool] = Field(default=None) + color_by: Optional[str] = Field(default=None) + color_by_component: Optional[int] = Field(default=None) + diffuse_rgb: Optional[List[float]] = Field(default=None) diff --git a/src/ansys/visor/viewer/models/common/visor_camera_state.py b/src/ansys/visor/viewer/models/common/visor_camera_state.py new file mode 100644 index 00000000..b69639eb --- /dev/null +++ b/src/ansys/visor/viewer/models/common/visor_camera_state.py @@ -0,0 +1,20 @@ +"""Model for camera state in a Visor visualization.""" + +from typing import List + +from pydantic import BaseModel, ConfigDict, Field + + +class VisorCameraState(BaseModel): + """ + Represents the camera settings in a Visor visualization. + """ + model_config = ConfigDict(populate_by_name=True) + + position: List[float] + focal_point: List[float] = Field(alias="focalPoint") + view_up: List[float] = Field(alias="viewUp") + clipping_range: List[float] = Field(alias="clippingRange") + parallel_projection: bool = Field(alias="parallelProjection") + view_angle: float = Field(alias="viewAngle") + parallel_scale: float = Field(alias="parallelScale") diff --git a/src/ansys/visor/viewer/models/common/visor_cross_section_state.py b/src/ansys/visor/viewer/models/common/visor_cross_section_state.py new file mode 100644 index 00000000..0f9fd2c5 --- /dev/null +++ b/src/ansys/visor/viewer/models/common/visor_cross_section_state.py @@ -0,0 +1,14 @@ +"""Model for cross section widget state in a Visor visualization.""" + +from typing import List + +from pydantic import BaseModel + + +class VisorCrossSectionState(BaseModel): + """ + Represents the cross-section widget state in a Visor visualization. + """ + origin: List[float] | None = None + normal: List[float] | None = None + diff --git a/src/ansys/visor/viewer/models/common/visor_ui_state.py b/src/ansys/visor/viewer/models/common/visor_ui_state.py new file mode 100644 index 00000000..fefa93b3 --- /dev/null +++ b/src/ansys/visor/viewer/models/common/visor_ui_state.py @@ -0,0 +1,16 @@ +"""Model for UI state in a Visor visualization.""" + +from pydantic import BaseModel, ConfigDict, Field + + +class VisorUIState(BaseModel): + """ + Represents the UI state of a Visor visualization, such as theme preference. + """ + model_config = ConfigDict(populate_by_name=True) + + dark_theme: bool | None = Field(default=None, alias="darkTheme") + panel_top_left_panel_collapsed: bool | None = Field(default=None, alias="panelTopLeftPanelCollapsed") + panel_top_right_panel_collapsed: bool | None = Field(default=None, alias="panelTopRightPanelCollapsed") + panel_top_right_legend_collapsed: bool | None = Field(default=None, alias="panelTopRightLegendCollapsed") + panel_top_right_tab_index: int | None = Field(default=None, alias="panelTopRightTabIndex") diff --git a/src/ansys/visor/viewer/models/common/visor_variable_state.py b/src/ansys/visor/viewer/models/common/visor_variable_state.py new file mode 100644 index 00000000..4034689a --- /dev/null +++ b/src/ansys/visor/viewer/models/common/visor_variable_state.py @@ -0,0 +1,25 @@ +"""Model for metadata for a variable on a dataset part in a Visor visualization.""" + +from typing import List, Tuple + +from pydantic import BaseModel, ConfigDict, Field + + +class VisorVariableState(BaseModel): + """ + Represents the state of all Visor variables, currently including the ranges + for each individual component, and for the magnitude. + + Note: Currently the frontend aggregates global variables across all datasets and dataset parts loaded in the + scene at a given time. It generates the ID from three properties that, combined, form + a unique identifier for the variable: the variable name, type (POINT/CELL), and number of components, and computes + the ranges across all datasets/parts that contain that variable. + + In the future, the backend can own this, but for now we can treat this as passthrough data, + as the id value is stable across sessions. + """ + model_config = ConfigDict(populate_by_name=True) + + id: str = Field(...) + magnitude_range: Tuple[float, float] | None = Field(default=None, alias="magnitudeRange") + ranges: List[Tuple[float, float]] = Field(default_factory=list) diff --git a/src/ansys/visor/viewer/models/info/__init__.py b/src/ansys/visor/viewer/models/info/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/info/visor_dataset_info.py b/src/ansys/visor/viewer/models/info/visor_dataset_info.py new file mode 100644 index 00000000..d2518bd5 --- /dev/null +++ b/src/ansys/visor/viewer/models/info/visor_dataset_info.py @@ -0,0 +1,15 @@ +"""Info for a Visor dataset.""" + +from pydantic import BaseModel, Field + + +class VisorDatasetInfo(BaseModel): + """ + Info for a Visor dataset. + """ + id: int = Field(...) + name: str = Field(...) + unit: str = Field(...) + file_path: str | None = None + metadata_path: str | None = None + diff --git a/src/ansys/visor/viewer/models/info/visor_variable_info.py b/src/ansys/visor/viewer/models/info/visor_variable_info.py new file mode 100644 index 00000000..f1e480ee --- /dev/null +++ b/src/ansys/visor/viewer/models/info/visor_variable_info.py @@ -0,0 +1,34 @@ +"""Model for a VTK variable array.""" + +from typing import List, Tuple + +from pydantic import BaseModel, ConfigDict, Field, field_serializer + +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType + + +class VisorVariableInfo(BaseModel): + """ + Metadata for a VTK variable array. + Attributes: + index_for_type (int): Index of the variable within its type (point or cell). + type (VisorVtkVariableType): Type of the variable (e.g., POINT or CELL). + name (str): Name of the variable. + num_components (int): Number of components in the variable array. + ranges (List[Tuple[float, float]]): List of ranges for each component. + magnitude_range (Tuple[float, float] | None): Range of magnitudes if applicable. + """ + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + index_for_type: int = Field(alias="indexForType") + type: VisorVtkVariableType + name: str + num_components: int = Field(alias="numComponents") + ranges: List[Tuple[float, float]] + magnitude_range: Tuple[float, float] | None = Field(default=None, alias="magnitudeRange") + + @field_serializer("type") + def _serialize_type(self, value: VisorVtkVariableType) -> str: + """Emit the wire value (e.g. "POINT"/"CELL") for both dict-mode and JSON-mode dumps.""" + return value.value + diff --git a/src/ansys/visor/viewer/models/persist/__init__.py b/src/ansys/visor/viewer/models/persist/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/persist/dataset/__init__.py b/src/ansys/visor/viewer/models/persist/dataset/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/persist/dataset/persisted_dataset_state.py b/src/ansys/visor/viewer/models/persist/dataset/persisted_dataset_state.py new file mode 100644 index 00000000..c204eb8b --- /dev/null +++ b/src/ansys/visor/viewer/models/persist/dataset/persisted_dataset_state.py @@ -0,0 +1,46 @@ +"""Persisted state for a single dataset (cross-session).""" + +from typing import Dict + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from ansys.visor.viewer.models.common.part_properties import PartProperties + + +class PersistedDatasetState(BaseModel): + """Persisted state for a single dataset (cross-session). + + Fields + ------ + - `parts` is keyed by stable *part names*. + - `serialized_dataset_path` points at the serialized dataset written under the + user-provided save directory (so in-mem/mutated datasets can be restored). + - `source_file_path` / `source_metadata_path` (when present) are the original + paths the dataset/metadata came from when first loaded. + + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + parts: Dict[str, PartProperties] = Field( + default_factory=dict, + description="Per-part state keyed by part name (not internal ID)" + ) + # Path to the dataset snapshot written alongside visor.json. + serialized_dataset_path: str | None = Field( + default=None, + description="Relative or absolute path to the serialized dataset snapshot in the save-state directory", + ) + # Original source references (if the dataset/metadata were loaded from disk) + source_file_path: str | None = Field(default=None, description="Original dataset file path provided by the user") + source_metadata_path: str | None = Field(default=None, description="Original metadata JSON path provided by the user") + + + @model_validator(mode="after") + def ensure_parts(self) -> "PersistedDatasetState": + """Ensure that all parts are present in the dataset.""" + if isinstance(self.parts, dict): + self.parts = { + k: v if isinstance(v, PartProperties) else PartProperties(**v) + for k, v in self.parts.items() + } + return self diff --git a/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py b/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py new file mode 100644 index 00000000..16161113 --- /dev/null +++ b/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py @@ -0,0 +1,69 @@ +"""Model for persisting Visor viewer state across sessions.""" + + +from typing import Dict, Literal + +from pydantic import BaseModel, ConfigDict, Field + +from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState +from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState +from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState +from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState +from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState +from ansys.visor.viewer.models.persist.scene.persisted_scene_state import PersistedSceneState + + +class PersistedViewerStateV1(BaseModel): + """ + Versioned container for persisting Visor viewer state across sessions. + + Contains only stable identifiers (dataset names, part names) without + session-specific IDs or WASM references. + + TODO: Extend in future versions to include more state information + such as camera position, active variables, UI settings, etc. + """ + model_config = ConfigDict(arbitrary_types_allowed=True) + + version: Literal["1.0"] = Field(default="1.0") + + ui: VisorUIState = Field(default_factory=VisorUIState) + scene: PersistedSceneState = Field(default_factory=PersistedSceneState) + + @classmethod + def from_components(cls, + ui_state: VisorUIState, + unit: str | None, + orthographic_enabled: bool | None, + cross_section_enabled: bool | None, + edges_enabled: bool | None, + bounding_box_enabled: bool | None, + datasets: Dict[str, PersistedDatasetState], + camera: VisorCameraState | None = None, + cross_section: VisorCrossSectionState | None = None, + variable_states: Dict[str, "VisorVariableState"] | None = None, + ) -> "PersistedViewerStateV1": + """ + Create a PersistedViewerStateV1 instance from UI settings and dataset states. + + Args: + ui_state (VisorUIState): UI settings object. + unit (str | None): Scene unit (or None). + datasets (Dict[str, PersistedDatasetState]): Dataset states keyed by dataset name. + camera (VisorCameraState | None): Camera state (or None). + variable_states (Dict[str, VisorVariableState] | None): Variable states keyed by variable identifier (or None). + Returns: + PersistedViewerStateV1: The constructed viewer state. + """ + scene_state = PersistedSceneState( + unit=unit, + camera=camera, + cross_section=cross_section, + orthographic_enabled=orthographic_enabled, + cross_section_enabled=cross_section_enabled, + edges_enabled=edges_enabled, + bounding_box_enabled=bounding_box_enabled, + dataset_states=datasets, + variable_states=variable_states or {}, + ) + return cls(ui=ui_state, scene=scene_state) diff --git a/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py new file mode 100644 index 00000000..be12ddd3 --- /dev/null +++ b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py @@ -0,0 +1,34 @@ +"""Model for persisted scene state (cross-session).""" + +from typing import Dict + +from pydantic import BaseModel, ConfigDict, Field + +from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState +from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState +from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState +from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState + + +class PersistedSceneState(BaseModel): + """Persisted (cross-session) scene state. + + This model is part of the on-disk save/load format and must contain only + stable identifiers. + + Notes: + - ``dataset_states`` is keyed by **dataset name** (string), which is a stable identifier across sessions. + - ``variable_states`` is keyed by **variable ID** (string), which is a stable identifier across sessions. + """ + + unit: str | None = None + camera: VisorCameraState | None = None + cross_section: VisorCrossSectionState | None = None + orthographic_enabled: bool | None = None + cross_section_enabled: bool | None = None + edges_enabled: bool | None = None + bounding_box_enabled: bool | None = None + camera: VisorCameraState | None = None + dataset_states: Dict[str, "PersistedDatasetState"] = Field(default_factory=dict) + variable_states: Dict[str, "VisorVariableState"] = Field(default_factory=dict) + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/src/ansys/visor/viewer/models/runtime/__init__.py b/src/ansys/visor/viewer/models/runtime/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/runtime/dataset/__init__.py b/src/ansys/visor/viewer/models/runtime/dataset/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py b/src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py new file mode 100644 index 00000000..5012f1d1 --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py @@ -0,0 +1,145 @@ +"""Model for runtime dataset state serialization.""" + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_serializer + +from ansys.visor.viewer.models.common.part_properties import PartProperties + + +class RuntimePartProperties(BaseModel): + """ + Frontend-facing model of part properties for runtime state serialization. + + This class mirrors the fields of ``PartProperties`` using snake_case Python + attribute names, but serializes with camelCase aliases (``spectrumId``, + ``spectrumComponent``, ``diffuseRgb``) for parity with the frontend wire + format. Callers must pass ``by_alias=True`` when dumping (e.g., in + ``get_scene_details_json``) so the emitted keys stay camelCase. + + Serialization semantics (aligned with the frontend state model): + + - ``spectrumId`` is **always included** in the serialized output, even when + ``None``. On the frontend, ``null`` means "no spectrum applied" (a real + value), while an absent key means "leave this property unchanged". + - All other optional fields (``opacity``, ``visible``, ``selected``, + ``spectrumComponent``, ``diffuseRgb``) are **omitted from the serialized + output when ``None``**, so that the frontend treats them as pass-through + / unchanged. + + id: int + opacity: Optional[float]: The opacity of the part, between 0.0 (fully transparent) and 1.0 (fully opaque). + visible: Optional[bool]: Whether the part is visible in the scene. + selected: Optional[bool]: Whether the part is currently selected by the user. + spectrum_id: Optional[str]: The ID of the variable used to colour this part, + or ``None`` to indicate that no spectrum is applied. Always serialized. + spectrum_component: Optional[int]: If the variable specified by spectrum_id has multiple components, + this specifies which component to use for coloring. + """ + model_config = ConfigDict(populate_by_name=True) + + id: int + opacity: Optional[float] = Field(default=None, ge=0.0, le=1.0) + visible: Optional[bool] = Field(default=None) + selected: Optional[bool] = Field(default=None) + + # spectrum_id=None means "no spectrum applied" โ€” a real, meaningful value. + # It is always included in serialized output so the frontend can act on it. + spectrum_id: Optional[str] = Field(default=None, alias="spectrumId") + # If the variable with ID spectrum_id has multiple components, this specifies which component to use for coloring. + spectrum_component: Optional[int] = Field(default=None, alias="spectrumComponent") + + # If set, this part's color is determined by the specified RGB values (each between 0 and 1). + diffuse_rgb: Optional[List[float]] = Field(default=None, alias="diffuseRgb") + + @model_serializer(mode="wrap") + def _serialize(self, handler: Any, info: Any = None) -> dict: + """Custom serializer that implements the frontend undefined-vs-null contract. + + ``spectrumId`` is always present in the output (``null`` is meaningful). + All other optional fields are omitted when their value is ``None``, so + the frontend interprets them as pass-through / undefined. + + The set of "omit-when-None" keys is checked against **both** the + snake_case attribute names and their camelCase aliases so this works + regardless of whether the caller uses ``by_alias=True``. + """ + data: dict = handler(self) + keys_to_omit_when_none = { + "opacity", "visible", "selected", + "spectrum_component", "spectrumComponent", + "diffuse_rgb", "diffuseRgb", + } + return {k: v for k, v in data.items() if not (k in keys_to_omit_when_none and v is None)} + + def to_part_properties(self) -> PartProperties: + return PartProperties( + opacity=self.opacity, + visible=self.visible, + selected=self.selected, + color_by=self.spectrum_id, + color_by_component=self.spectrum_component, + diffuse_rgb=self.diffuse_rgb, + ) + + @classmethod + def from_part_properties(cls, id: int, props: PartProperties) -> "RuntimePartProperties": + return cls( + id=id, + opacity=props.opacity, + visible=props.visible, + selected=props.selected, + spectrum_id=props.color_by, + spectrum_component=props.color_by_component, + diffuse_rgb=props.diffuse_rgb, + ) + + +class RuntimeDatasetState(BaseModel): + """ + Represents the runtime, in-memory state of a Visor visualization, + including properties for individual parts. + + The part dictionary is keyed by internal dataset IDs. + + This class is for serialization of the runtime state that is sent + to the frontend viewer. + """ + model_config = ConfigDict(populate_by_name=True) + + id: int + part_states: Dict[int, "RuntimePartProperties"] = Field(default_factory=dict, alias="partStates") + + @field_validator("part_states", mode="before") + @classmethod + def _coerce_part_states(cls, v: Any) -> Any: + if v is None: + return {} + + # Common case: JSON object -> dict with string keys + if isinstance(v, dict): + out: Dict[int, Any] = {} + for k, val in v.items(): + try: + ik = int(k) + except (TypeError, ValueError): + ik = k # type: ignore[assignment] + out[ik] = val + return out + + return v + + @classmethod + def from_components( + cls, + id: int, + part_states: Dict[int, PartProperties], + ) -> "RuntimeDatasetState": + runtime_part_states = { + part_id: RuntimePartProperties.from_part_properties(id=part_id, props=props) \ + for part_id, props in part_states.items() + } + return cls( + id=id, + part_states=runtime_part_states, + ) diff --git a/src/ansys/visor/viewer/models/runtime/requests/visor_load_state_request.py b/src/ansys/visor/viewer/models/runtime/requests/visor_load_state_request.py new file mode 100644 index 00000000..a91bca7f --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/requests/visor_load_state_request.py @@ -0,0 +1,16 @@ +"""Model for loading a Visor viewer state.""" + +from pydantic import BaseModel, ConfigDict, Field + +from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState + + +class VisorLoadStateRequest(BaseModel): + """ + Request model for loading a Visor viewer state. + """ + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + # A unique identifier for the load state request. + request_id: int + app_state: RuntimeAppState = Field(alias="appState") diff --git a/src/ansys/visor/viewer/models/runtime/requests/visor_save_state_request.py b/src/ansys/visor/viewer/models/runtime/requests/visor_save_state_request.py new file mode 100644 index 00000000..1d72098a --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/requests/visor_save_state_request.py @@ -0,0 +1,15 @@ +"""Model for sending a request to the client to retrieve the runtime viewer state.""" + +from pydantic import BaseModel, ConfigDict, Field + + +class VisorSaveStateRequest(BaseModel): + """ + Model for sending a request to the client to retrieve the runtime + viewer state, which is in turn used by the server to save the persisted state of the + Visor viewer. + """ + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + # A unique identifier for the load state request. + request_id: int = Field(alias="requestId") diff --git a/src/ansys/visor/viewer/models/runtime/requests/visor_save_state_response.py b/src/ansys/visor/viewer/models/runtime/requests/visor_save_state_response.py new file mode 100644 index 00000000..a3929015 --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/requests/visor_save_state_response.py @@ -0,0 +1,37 @@ +import json +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState + + +class VisorSaveStateResponse(BaseModel): + """ + Model for the response from the client to receive the runtime + viewer state, which is in turn used by the server to save the persisted state of the + Visor viewer. + """ + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + # A unique identifier for the load state request. + request_id: int = Field(alias="requestId") + app_state: RuntimeAppState = Field(alias="appState") + + @field_validator("app_state", mode="before") + @classmethod + def _coerce_app_state(cls, v: Any) -> Any: + """Coerce the app_state value to an Any type, allowing for flexible input formats.""" + # Accept already-built model + if isinstance(v, RuntimeAppState): + return v + + # Accept JSON string + if isinstance(v, str): + v = json.loads(v) + + # Accept dict and let Pydantic recursively build RuntimeAppState + nested models + if isinstance(v, dict): + return RuntimeAppState.model_validate(v) + + return v diff --git a/src/ansys/visor/viewer/models/runtime/scene/__init__.py b/src/ansys/visor/viewer/models/runtime/scene/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py b/src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py new file mode 100644 index 00000000..4eb9512d --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py @@ -0,0 +1,60 @@ +"""Model for the runtime application state for a Visor visualizer instance.""" + +from typing import Dict + +from pydantic import BaseModel, ConfigDict, Field + +from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState +from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState +from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState +from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState +from ansys.visor.viewer.models.runtime.scene.runtime_scene_state import RuntimeSceneState + + +class RuntimeAppState(BaseModel): + """ + Represents the runtime, in-memory state of a Visor visualization, + including properties for individual parts. + + The part dictionary is keyed by internal dataset IDs. + + This class is for serialization of the runtime state that is sent + to the frontend viewer. + """ + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + ui: VisorUIState = Field(default_factory=VisorUIState) + scene: RuntimeSceneState = Field(default_factory=RuntimeSceneState) + + @classmethod + def from_components( + cls, + dark_mode: bool, + unit: str | None, + dataset_states: Dict[int, RuntimeDatasetState], + orthographic_enabled: bool | None = None, + cross_section_enabled: bool | None = None, + edges_enabled: bool | None = None, + bounding_box_enabled: bool | None = None, + cross_section: VisorCrossSectionState | None = None, + camera: VisorCameraState | None = None, + variable_states: Dict[str, VisorVariableState] | None = None, + ) -> "RuntimeAppState": + """Construct a RuntimeAppState from the given components.""" + ui_state = VisorUIState(dark_theme=dark_mode) + runtime_scene_state = RuntimeSceneState( + unit=unit, + camera=camera, + cross_section=cross_section, + orthographic_enabled=orthographic_enabled, + cross_section_enabled=cross_section_enabled, + edges_enabled=edges_enabled, + bounding_box_enabled=bounding_box_enabled, + dataset_states=dataset_states, + spectrum_states=variable_states or {}, + ) + return cls( + ui=ui_state, + scene=runtime_scene_state, + ) diff --git a/src/ansys/visor/viewer/models/runtime/scene/runtime_scene_state.py b/src/ansys/visor/viewer/models/runtime/scene/runtime_scene_state.py new file mode 100644 index 00000000..a0b8bc6a --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/scene/runtime_scene_state.py @@ -0,0 +1,54 @@ +"""Model for runtime state of the viusalizer scene.""" + +from typing import Any, Dict + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState +from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState +from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState + + +class RuntimeSceneState(BaseModel): + """ + Represents the runtime, in-memory state of a Visor visualization scene, + including properties for individual datasets. + The dataset dictionary is keyed by internal dataset IDs. + This class is for serialization of the runtime state that is sent + to the frontend viewer (not to be confused with persisted state, which is stable between viewer sessions). + """ + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + unit: str | None = None + camera: VisorCameraState | None = None + cross_section: VisorCrossSectionState | None = Field(default=None, alias="crossSection") + orthographic_enabled: bool | None = Field(default=None, alias="orthographicEnabled") + cross_section_enabled: bool | None = Field(default=None, alias="crossSectionEnabled") + edges_enabled: bool | None = Field(default=None, alias="edgesEnabled") + bounding_box_enabled: bool | None = Field(default=None, alias="boundingBoxEnabled") + dataset_states: Dict[int, "RuntimeDatasetState"] = Field(default_factory=dict, alias="datasetStates") + spectrum_states: Dict[str, "VisorVariableState"] = Field(default_factory=dict, alias="spectrumStates") + + @field_validator("dataset_states", mode="before") + @classmethod + def _coerce_dataset_states(cls, v: Any) -> Any: + """Coerce the given dataset state to an appropriate type.""" + # Accept None / missing + if v is None: + return {} + + # If already a dict, coerce keys to int (JSON object keys arrive as str) + if isinstance(v, dict): + out: Dict[int, Any] = {} + for k, val in v.items(): + try: + ik = int(k) + except (TypeError, ValueError): + # Leave key as-is if it cannot be coerced; Pydantic will raise later if invalid + ik = k # type: ignore[assignment] + out[ik] = val + return out + + # Otherwise, let Pydantic try its normal parsing/validation + return v diff --git a/src/ansys/visor/viewer/models/runtime/visor_scene_details.py b/src/ansys/visor/viewer/models/runtime/visor_scene_details.py new file mode 100644 index 00000000..6f4a25cf --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/visor_scene_details.py @@ -0,0 +1,51 @@ +"""Model for visor scene details, the data transfer object sent to the client.""" + +from typing import Dict + +from pydantic import BaseModel, ConfigDict, Field + +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState +from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState +from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import RendererAnnotation +from ansys.visor.viewer.models.runtime.vtk.runtime_vtk_info import RuntimeVTKInfo +from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo + +# Bumped whenever the scene-details wire shape changes incompatibly. +SCENE_DETAILS_SCHEMA_VERSION: int = 2 + + +class VisorSceneDetails(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + schema_version: int = Field( + default=SCENE_DETAILS_SCHEMA_VERSION, alias="schemaVersion" + ) + app_state: RuntimeAppState | None = Field(default=None, alias="appState") + vtk_info: RuntimeVTKInfo | None = Field(default=None, alias="vtkInfo") + + @classmethod + def from_components( + cls, + dark_mode: bool, + unit: str | None, + dataset_states: Dict[int, RuntimeDatasetState], + scene_graph_state: SceneGraphNodeInfo | None = None, + renderer_annotation: RendererAnnotation | None = None, + ) -> "VisorSceneDetails": + """Construct an instance from components.""" + vtk_info = RuntimeVTKInfo( + scene_graph=scene_graph_state, + renderer_annotation=renderer_annotation, + ) + app_state = RuntimeAppState.from_components( + dark_mode=dark_mode, + unit=unit, + dataset_states=dataset_states, + ) + return cls( + app_state=app_state, + vtk_info=vtk_info, + ) + + +VisorSceneDetails.model_rebuild() diff --git a/src/ansys/visor/viewer/models/runtime/vtk/__init__.py b/src/ansys/visor/viewer/models/runtime/vtk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/models/runtime/vtk/renderer_annotation.py b/src/ansys/visor/viewer/models/runtime/vtk/renderer_annotation.py new file mode 100644 index 00000000..c5b6d7f6 --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/vtk/renderer_annotation.py @@ -0,0 +1,60 @@ +"""Models for the renderer annotation half of the scene-details payload. + +The annotation carries every renderer-specific handle +a client needs in order to bind to the scene description: per-node +handles for registered part nodes, and handles for the renderer's singleton +widgets. +""" + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class RendererAnnotation(BaseModel): + """Base for every renderer's annotation. + + Discriminated on the wire by ``rendererKind`` so the client can narrow to a + concrete type at the parse boundary without a permissive union. + """ + model_config = ConfigDict(populate_by_name=True) + + renderer_kind: str = Field(alias="rendererKind") + + +class WasmNodeHandles(BaseModel): + """Wasm object-manager ids for one renderable part node.""" + model_config = ConfigDict(populate_by_name=True) + + actor_id: int = Field(alias="actorId") + property_id: int = Field(alias="propertyId") + mapper_id: int = Field(alias="mapperId") + + +class WasmWidgetHandles(BaseModel): + """Wasm object-manager ids for the renderer's singleton widgets. + + Static after renderer initialisation; independent of scene contents. + """ + model_config = ConfigDict(populate_by_name=True) + + orientation_widget_id: int = Field(alias="orientationWidgetId") + cross_section_plane_id: int = Field(alias="crossSectionPlaneId") + cross_section_plane_widget_id: int = Field(alias="crossSectionPlaneWidgetId") + cross_section_plane_representation_id: int = Field( + alias="crossSectionPlaneRepresentationId" + ) + bounding_box_algorithm_id: int = Field(alias="boundingBoxAlgorithmId") + bounding_box_outline_actor_id: int = Field(alias="boundingBoxOutlineActorId") + bounding_box_axes_actor_id: int = Field(alias="boundingBoxAxesActorId") + + +class WasmRendererAnnotation(RendererAnnotation): + """Annotation produced by :class:`VisorLocalRenderer`.""" + + renderer_kind: Literal["wasm"] = Field(default="wasm", alias="rendererKind") + # Keyed by str(node_id): JSON object keys must be strings. + # Only part nodes appear. Group and root nodes have no renderable objects. + nodes: dict[str, WasmNodeHandles] = Field(default_factory=dict) + widgets: WasmWidgetHandles + diff --git a/src/ansys/visor/viewer/models/runtime/vtk/runtime_vtk_info.py b/src/ansys/visor/viewer/models/runtime/vtk/runtime_vtk_info.py new file mode 100644 index 00000000..d1569b78 --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/vtk/runtime_vtk_info.py @@ -0,0 +1,25 @@ +"""Model for the runtime VTK info.""" + +from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny + +from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import RendererAnnotation +from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo + + +class RuntimeVTKInfo(BaseModel): + """ + Represents the runtime, in-memory VTK-specific state of a Visor visualization scene. + This class is for serialization of the runtime VTK state that is sent + to the frontend viewer. + """ + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + scene_graph: SceneGraphNodeInfo | None = Field(default=None, alias="sceneGraph") + # Must be SerializeAsAny so pydantic v2 serializes the concrete subclass's + # extra fields (`nodes`, `widgets`) rather than only the base's `rendererKind`. + renderer_annotation: SerializeAsAny[RendererAnnotation] | None = Field( + default=None, alias="rendererAnnotation" + ) + + +RuntimeVTKInfo.model_rebuild() diff --git a/src/ansys/visor/viewer/models/runtime/vtk/scene_graph_node_info.py b/src/ansys/visor/viewer/models/runtime/vtk/scene_graph_node_info.py new file mode 100644 index 00000000..d562722d --- /dev/null +++ b/src/ansys/visor/viewer/models/runtime/vtk/scene_graph_node_info.py @@ -0,0 +1,26 @@ +"""Model for scene graph node info.""" + +from typing import List + +from pydantic import BaseModel, ConfigDict, Field + +from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.models.info.visor_variable_info import VisorVariableInfo + + +class SceneGraphNodeInfo(BaseModel): + """Model for scene graph node info.""" + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + id: int + name: str + is_actor_node: bool = Field(alias="isActorNode") + is_group_node: bool = Field(alias="isGroupNode") + data_arrays: list[VisorVariableInfo] = Field(default_factory=list, alias="dataArrays") + node_type: str = Field(alias="nodeType") + bounds: list = Field(default_factory=list) + children: List['SceneGraphNodeInfo'] = Field(default_factory=list) + diffuse_color: VisorColors = Field(default=VisorColors.DefaultMeshColor, alias="diffuseColor") + + +SceneGraphNodeInfo.model_rebuild() diff --git a/src/ansys/visor/viewer/renderer/__init__.py b/src/ansys/visor/viewer/renderer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/renderer/base.py b/src/ansys/visor/viewer/renderer/base.py new file mode 100644 index 00000000..0d9cc9b9 --- /dev/null +++ b/src/ansys/visor/viewer/renderer/base.py @@ -0,0 +1,303 @@ +""" +IRenderer +========= +Abstract protocol for the Visor rendering backend. + +The scene coordinator programs against this interface. Swapping backends +(VTK/wasm local view -> future non-wasm) means providing a new implementation +without touching scene coordination. + +Contract covers node lifecycle, per-part visual mutations, +camera, widget control (cross-section, bounding box), widget fan-out (scene +bounds, actor count), picking, and render/flush. Not covered yet: +state-authority hooks, trigger-facing methods, round-trip additions. + +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from vtkmodules.vtkCommonDataModel import vtkDataObject + + from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState + from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import RendererAnnotation + from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphPartNode + + +class IRenderer(ABC): + """Abstract rendering backend consumed by the scene coordinator.""" + + # ------------------------------------------------------------------------ + # Wire contract + # ------------------------------------------------------------------------ + + @abstractmethod + def build_renderer_annotation(self) -> "RendererAnnotation | None": + """Return this renderer's annotation for the current scene, or ``None``. + + The annotation carries every renderer-specific handle a client needs in + order to bind to the scene description: per-node handles for registered + part nodes, and handles for the renderer's singleton widgets. + + ``None`` for a renderer with no client-side handles -- absence is the + wire representation, not a value. + + The return type is concrete per renderer and is discriminated on the wire + by ``rendererKind``. It is never a free-form dict. + + Contract, load-bearing for the geometry picker: the per-node actor handle + MUST be the value returned by the renderer's own object manager for the + registered pipeline's actor -- the same call that produces the id the wasm + picker reports at click time. It is never re-keyed or re-indexed. + """ + + # ------------------------------------------------------------------------ + # Node lifecycle + # ------------------------------------------------------------------------ + + @abstractmethod + def register_node( + self, node: "VisorSceneGraphPartNode", dataset: "vtkDataObject" + ) -> None: + """ + Build the VTK pipeline for *node* from *dataset* and register its actor. + + The renderer owns the resulting :class:`VtkNodePipeline` and is the + sole caller of :meth:`VtkNodePipeline.from_dataset`. Must be + idempotent: calling twice for the same node must not create a + duplicate actor. + """ + + @abstractmethod + def deregister_node(self, node_id: int) -> None: + """ + Remove the actor for *node_id* and destroy its pipeline. + + Silently does nothing when *node_id* is not registered. + """ + + @abstractmethod + def deregister_all(self) -> None: + """Deregister every registered node and destroy every pipeline. + + Equivalent to calling :meth:`deregister_node` for every currently + registered node id. Idempotent; safe on an empty registry. + """ + + # ------------------------------------------------------------------------ + # Per-part visual mutations + # ------------------------------------------------------------------------ + + @abstractmethod + def apply_visibility(self, node_id: int, visible: bool) -> None: + """Set actor visibility for *node_id*.""" + + @abstractmethod + def apply_opacity(self, node_id: int, opacity: float) -> None: + """Set actor opacity for *node_id*.""" + + @abstractmethod + def apply_diffuse_color( + self, node_id: int, r: float, g: float, b: float + ) -> None: + """Set the actor-property diffuse colour for *node_id*.""" + + @abstractmethod + def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: + """Toggle edge / wireframe visibility for *node_id*.""" + + @abstractmethod + def apply_selected( + self, node_id: int, selected: bool, diffuse_rgb: list + ) -> None: + """ + Apply or remove the selection highlight on *node_id*. + + *diffuse_rgb* is the part's stored diffuse colour, used when + deselecting to restore the property DiffuseColor alongside resetting + Ambient/Diffuse. + """ + + @abstractmethod + def apply_color_variable( + self, + node_id: int, + spectrum_id: str, + array_type: str, + array_name: str, + component: int, + min_val: float, + max_val: float, + ) -> None: + """ + Configure the mapper to colour *node_id* by the given scalar array + and range. + + Parameters + ---------- + node_id: + Scene-graph node to update. + spectrum_id: + Server-authoritative opaque ID (stored as-is; not parsed here). + array_type: + ``"POINT"`` or ``"CELL"``. + array_name: + VTK array name. + component: + Component index. ``-1`` means magnitude. + min_val / max_val: + Scalar display range. + """ + + @abstractmethod + def clear_color_variable(self, node_id: int) -> None: + """Disable scalar colouring on *node_id*, reverting to solid diffuse.""" + + @abstractmethod + def refresh_color_variable_range( + self, + node_id: int, + spectrum_id: str, + array_type: str, + array_name: str, + component: int, + ) -> None: + """ + Update only the mapper's scalar range for *node_id* from the current + input data, without calling ``pipeline.base_algorithm.Update()``. + + Safe to call after in-place data modifications where VTK's pipeline + MTime is stale. + """ + + # ------------------------------------------------------------------------ + # Camera + # ------------------------------------------------------------------------ + + @abstractmethod + def reset_camera(self, bounds: list[float]) -> None: + """Reset the camera to fit *bounds*.""" + + @abstractmethod + def get_camera_state(self) -> "VisorCameraState | None": + """ + Return the last camera state synced from the frontend, or ``None`` if + none has been received. + """ + + @abstractmethod + def sync_camera(self, camera_state: "VisorCameraState") -> None: + """Store the camera state synced back from the frontend.""" + + # ------------------------------------------------------------------------ + # Widget control (cross-section, bounding box) + # ------------------------------------------------------------------------ + + @abstractmethod + def set_cross_section_visibility(self, visible: bool) -> None: + """Show or hide the cross-section clipping plane.""" + + @abstractmethod + def sync_cross_section_plane( + self, origin: list[float], normal: list[float] + ) -> None: + """ + Sync the cross-section plane origin and normal from the frontend back + to server-side VTK objects. + """ + + @abstractmethod + def set_bounding_box_visibility(self, visible: bool) -> None: + """Show or hide the bounding-box outline.""" + + # ------------------------------------------------------------------------ + # Widget fan-out (renderer primitives -- coordinator computes, renderer + # dispatches to its owned widgets) + # ------------------------------------------------------------------------ + + @abstractmethod + def update_bounds(self, bounds: list[float]) -> None: + """ + Propagate scene bounds to bounds-consuming widgets + (cross-section, bounding box). + """ + + @abstractmethod + def update_actor_count(self, count: int) -> None: + """Propagate the current part-node count to widgets.""" + + # ------------------------------------------------------------------------ + # Picking + # ------------------------------------------------------------------------ + + @abstractmethod + def pick_geometry( + self, + actor_wasm_id: int, + cell_id: int, + mode: str, + world_pos: tuple[float, float, float], + ) -> dict: + """ + Resolve a client-initiated pick event to a sub-geometry payload. + + The renderer resolves the client-supplied opaque actor identifier to + an internal actor; the coordinator never performs that resolution. + + Parameters + ---------- + actor_wasm_id: + Client-supplied opaque actor identifier from the wasm-side picker. + cell_id: + Cell index within the picked actor's mapper dataset. + mode: + ``"vertex" | "edge" | "face"``. + world_pos: + World-space pick position from the client picker, used to select + the nearest vertex/edge for multi-vertex/edge cells. + + Returns + ------- + dict + Backend-neutral pick result. Always contains ``"found": bool``. + When ``found`` is ``True``, contains ``"mode"`` and mode-specific + fields (``position`` / ``pointA``+``pointB``+``length`` / + ``points``+``area``). + """ + + # ------------------------------------------------------------------------ + # Render / flush + # ------------------------------------------------------------------------ + + @abstractmethod + def render(self) -> None: + """Trigger a render and flush to the frontend.""" + + @abstractmethod + def render_window_only(self) -> None: + """Render the VTK window without flushing to the frontend.""" + + def flush_wasm_state(self) -> None: + """ + Serialise current Python VTK object state and push it to the wasm + client without triggering a VTK pipeline re-render. + + No-op default for non-wasm renderers. The local (wasm) renderer + overrides this to call ``local_view.update()``. + """ + + # ------------------------------------------------------------------------ + # Frontend addressing + # ------------------------------------------------------------------------ + + @property + @abstractmethod + def frontend_ref_name(self) -> str: + """Name by which the frontend view is addressed in ``server.js_call``. + + Empty string for renderers with no frontend view. + """ + diff --git a/src/ansys/visor/viewer/renderer/local_renderer.py b/src/ansys/visor/viewer/renderer/local_renderer.py new file mode 100644 index 00000000..4d07003a --- /dev/null +++ b/src/ansys/visor/viewer/renderer/local_renderer.py @@ -0,0 +1,455 @@ +""" +VisorLocalRenderer +================== +:class:`IRenderer` implementation for the wasm / :class:`LocalView` backend. + +Owns all VTK render-window / actor / widget / LocalView / object-manager +objects. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from trame_server import Server +from trame_vtklocal.widgets.vtklocal import LocalView +from vtkmodules.vtkCommonDataModel import vtkDataObject +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera +from vtkmodules.vtkRenderingCore import ( + vtkCellPicker, + vtkPropPicker, + vtkRenderer, + vtkRenderWindow, + vtkRenderWindowInteractor, +) +from vtkmodules.vtkSerializationManager import vtkObjectManager + +from ansys.visor.viewer.config import settings +from ansys.visor.viewer.core.perf_timer import PerfTimer +from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import ( + WasmNodeHandles, + WasmRendererAnnotation, + WasmWidgetHandles, +) +from ansys.visor.viewer.renderer.base import IRenderer +from ansys.visor.viewer.vtk.node_pipeline import VtkNodePipeline +from ansys.visor.viewer.vtk.widgets.visor_bounding_box import VisorBoundingBox +from ansys.visor.viewer.vtk.widgets.visor_cross_section import VisorCrossSectionWidget +from ansys.visor.viewer.vtk.widgets.visor_orientation import VisorOrientationWidget + +if TYPE_CHECKING: + from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState + from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphPartNode + +logger = VisorDefaultLogger(__name__) + + +class VisorLocalRenderer(IRenderer): + """IRenderer backend that owns the local wasm / LocalView VTK pipeline.""" + + _server: Server + _vtk_renderer: vtkRenderer + _render_window: vtkRenderWindow + _render_window_interactor: vtkRenderWindowInteractor + _local_view: LocalView + _object_manager: vtkObjectManager + _orientation_widget: VisorOrientationWidget + _cross_section_widget: VisorCrossSectionWidget + _bounding_box_widget: VisorBoundingBox + # Registered pipelines keyed by scene-graph node id. Sole owner as of + # Increment I5; mutated exclusively via register_node / deregister_node. + _pipelines: dict[int, VtkNodePipeline] + _last_camera_state: Optional["VisorCameraState"] + + def __init__(self, server: Server): + """Build the full local-mode VTK infrastructure and seed wasm state.""" + self._server = server + self._pipelines = {} + self._last_camera_state = None + + self._vtk_renderer = self._initialize_vtk_renderer() + self._render_window = self._initialize_render_window() + self._render_window_interactor = self._initialize_render_window_interactor() + self._local_view = self._initialize_local_view() + self._object_manager = self._local_view.object_manager + self._orientation_widget = self._initialize_orientation_widget() + self._cross_section_widget = self._initialize_cross_section_widget() + self._bounding_box_widget = self._initialize_bounding_box_widget() + + # Wasm state seeding -- moved verbatim from + # ``VisorScene._initialize_vtk_pipeline`` so the client-visible + # payload is byte-identical to main. + self._server.state["wasm_ids"] = { + "renderWindowId": self._local_view.get_wasm_id(self._render_window), + "rendererId": self._local_view.get_wasm_id(self._vtk_renderer), + "interactorId": self._local_view.register_vtk_object(self._render_window_interactor), + "pickerId": self._local_view.register_vtk_object(vtkPropPicker()), + } + self._server.state["wasm_ref_name"] = self._local_view.ref_name + self._server.state["perf_logging"] = settings.perf_logging + + + # ------------------------------------------------------------------ + # IRenderer: wire contract + # ------------------------------------------------------------------ + + def build_renderer_annotation(self) -> WasmRendererAnnotation: + """See :meth:`IRenderer.build_renderer_annotation`. + + Per-node handles are stamped from the same ``object_manager.GetId`` + call that :class:`SceneGraphStateBuilder` uses today to stamp + ``SceneGraphNodeInfo.wasm_actor_id`` etc. (the R2 accommodation + invariant) -- not a re-keyed or re-indexed id space. + """ + get_id = self._object_manager.GetId + nodes = { + str(node_id): WasmNodeHandles( + actor_id=get_id(pipe.actor), + property_id=get_id(pipe.actor.GetProperty()), + mapper_id=get_id(pipe.mapper), + ) + for node_id, pipe in self._pipelines.items() + } + widgets = WasmWidgetHandles( + orientation_widget_id=self._orientation_widget.widget_wasm_id, + cross_section_plane_id=self._cross_section_widget.plane_wasm_id, + cross_section_plane_widget_id=self._cross_section_widget.plane_widget_wasm_id, + cross_section_plane_representation_id=( + self._cross_section_widget.plane_representation_wasm_id + ), + bounding_box_algorithm_id=self._bounding_box_widget.box_algorithm_wasm_id, + bounding_box_outline_actor_id=self._bounding_box_widget.outline_wasm_actor_id, + bounding_box_axes_actor_id=self._bounding_box_widget.axes_wasm_actor_id, + ) + return WasmRendererAnnotation(nodes=nodes, widgets=widgets) + + # ------------------------------------------------------------------ + # IRenderer: node lifecycle + # ------------------------------------------------------------------ + + def register_node( + self, node: "VisorSceneGraphPartNode", dataset: vtkDataObject + ) -> None: + """See :meth:`IRenderer.register_node`.""" + node_id = node.id + if node_id in self._pipelines: + return # idempotent + pipe = VtkNodePipeline.from_dataset(dataset) + pipe.set_clipping_plane(self._cross_section_widget.plane) + pipe.mapper.SetScalarVisibility(False) + self._vtk_renderer.AddActor(pipe.actor) + self._pipelines[node_id] = pipe + + def deregister_node(self, node_id: int) -> None: + """See :meth:`IRenderer.deregister_node`.""" + pipe = self._pipelines.pop(node_id, None) + if pipe is not None: + self._vtk_renderer.RemoveActor(pipe.actor) + + def deregister_all(self) -> None: + """See :meth:`IRenderer.deregister_all`.""" + for node_id in list(self._pipelines.keys()): + self.deregister_node(node_id) + + # ------------------------------------------------------------------ + # IRenderer: per-part visual mutations + # ------------------------------------------------------------------ + + def apply_visibility(self, node_id: int, visible: bool) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def apply_opacity(self, node_id: int, opacity: float) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def apply_diffuse_color( + self, node_id: int, r: float, g: float, b: float + ) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def apply_selected( + self, node_id: int, selected: bool, diffuse_rgb: list + ) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def apply_color_variable( + self, + node_id: int, + spectrum_id: str, + array_type: str, + array_name: str, + component: int, + min_val: float, + max_val: float, + ) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def clear_color_variable(self, node_id: int) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def refresh_color_variable_range( + self, + node_id: int, + spectrum_id: str, + array_type: str, + array_name: str, + component: int, + ) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + # ------------------------------------------------------------------ + # IRenderer: camera + # ------------------------------------------------------------------ + + def reset_camera(self, bounds: list[float]) -> None: + """See :meth:`IRenderer.reset_camera`.""" + self._vtk_renderer.ResetCamera(bounds) + + def get_camera_state(self) -> Optional["VisorCameraState"]: + """See :meth:`IRenderer.get_camera_state`. + + Returns ``None`` on this branch: no coordinator caller and no + frontend round-trip populates the store. Phase 3 wires the sync. + """ + return self._last_camera_state + + def sync_camera(self, camera_state: "VisorCameraState") -> None: + """See :meth:`IRenderer.sync_camera`. + + Stores the state for :meth:`get_camera_state` to return. No + coordinator caller on this branch; Phase 3 wires the round-trip. + """ + self._last_camera_state = camera_state + + # ------------------------------------------------------------------ + # IRenderer: widget control (cross-section, bounding box) + # + # No coordinator caller on this branch. Phase 3 populates. + # ------------------------------------------------------------------ + + def set_cross_section_visibility(self, visible: bool) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def sync_cross_section_plane( + self, origin: list[float], normal: list[float] + ) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + def set_bounding_box_visibility(self, visible: bool) -> None: + """No-op in Story 1.2. Phase 3 populates.""" + + # ------------------------------------------------------------------ + # IRenderer: widget fan-out (called by coordinator today) + # ------------------------------------------------------------------ + + def update_bounds(self, bounds: list[float]) -> None: + """See :meth:`IRenderer.update_bounds`.""" + self._cross_section_widget.update_bounds(bounds) + self._bounding_box_widget.update_bounds(bounds) + + def update_actor_count(self, count: int) -> None: + """See :meth:`IRenderer.update_actor_count`.""" + self._bounding_box_widget.update_node_count(count) + + # ------------------------------------------------------------------ + # IRenderer: picking + # ------------------------------------------------------------------ + + def pick_geometry( + self, + actor_wasm_id: int, + cell_id: int, + mode: str, + world_pos: tuple[float, float, float], + ) -> dict: + """See :meth:`IRenderer.pick_geometry`. + + Body moved verbatim from ``VisorScene.pick_geometry``; only the + world-position packing changes (tuple in, three scalars out). + """ + import math + + actor_wasm_id = int(actor_wasm_id) + cell_id = int(cell_id) + pick_pos = [float(world_pos[0]), float(world_pos[1]), float(world_pos[2])] + + if actor_wasm_id < 0 or cell_id < 0: + return {"found": False} + + # Locate the actor by iterating the renderer's actors and matching + # WASM IDs (avoids depending on a GetObjectAtId reverse-lookup API). + actor = None + actors = self._vtk_renderer.GetActors() + actors.InitTraversal() + a = actors.GetNextActor() + while a is not None: + if self._object_manager.GetId(a) == actor_wasm_id: + actor = a + break + a = actors.GetNextActor() + + if actor is None: + return {"found": False} + + dataset = actor.GetMapper().GetInputDataObject(0, 0) + if dataset is None or cell_id >= dataset.GetNumberOfCells(): + return {"found": False} + + cell = dataset.GetCell(cell_id) + if cell is None: + return {"found": False} + + n_pts = cell.GetNumberOfPoints() + pts = cell.GetPoints() + + if mode == "vertex": + best_i = 0 + min_dist2 = float("inf") + for i in range(n_pts): + p = pts.GetPoint(i) + d2 = sum((pick_pos[k] - p[k]) ** 2 for k in range(3)) + if d2 < min_dist2: + min_dist2 = d2 + best_i = i + position = list(pts.GetPoint(best_i)) + return {"found": True, "mode": "vertex", "position": position} + + if mode == "edge": + if n_pts < 2: + return {"found": False} + best_i, best_j = 0, 1 + min_dist2 = float("inf") + for i in range(n_pts): + j = (i + 1) % n_pts + p0 = pts.GetPoint(i) + p1 = pts.GetPoint(j) + seg = [p1[k] - p0[k] for k in range(3)] + seg_len2 = sum(s * s for s in seg) + if seg_len2 > 0: + t = sum((pick_pos[k] - p0[k]) * seg[k] for k in range(3)) / seg_len2 + t = max(0.0, min(1.0, t)) + else: + t = 0.0 + closest = [p0[k] + t * seg[k] for k in range(3)] + d2 = sum((pick_pos[k] - closest[k]) ** 2 for k in range(3)) + if d2 < min_dist2: + min_dist2 = d2 + best_i, best_j = i, j + pt_a = list(pts.GetPoint(best_i)) + pt_b = list(pts.GetPoint(best_j)) + length = math.sqrt(sum((a - b) ** 2 for a, b in zip(pt_a, pt_b))) + return { + "found": True, + "mode": "edge", + "pointA": pt_a, + "pointB": pt_b, + "length": length, + } + + if mode == "face": + p0 = pts.GetPoint(0) + cx, cy, cz = 0.0, 0.0, 0.0 + for i in range(1, n_pts - 1): + a = pts.GetPoint(i) + b = pts.GetPoint(i + 1) + ax, ay, az = a[0] - p0[0], a[1] - p0[1], a[2] - p0[2] + bx, by, bz = b[0] - p0[0], b[1] - p0[1], b[2] - p0[2] + cx += ay * bz - az * by + cy += az * bx - ax * bz + cz += ax * by - ay * bx + area = 0.5 * math.sqrt(cx * cx + cy * cy + cz * cz) + points = [list(pts.GetPoint(i)) for i in range(n_pts)] + return {"found": True, "mode": "face", "area": area, "points": points} + + return {"found": False} + + # ------------------------------------------------------------------ + # IRenderer: render / flush + # ------------------------------------------------------------------ + + def render(self) -> None: + """See :meth:`IRenderer.render`.""" + timer = PerfTimer("render", logger) + with timer.phase("RenderWindow.Render"): + self._render_window.Render() + with timer.phase("LocalView.update"): + self._local_view.update() + timer.log() + + def render_window_only(self) -> None: + """See :meth:`IRenderer.render_window_only`.""" + self._render_window.Render() + + def flush_wasm_state(self) -> None: + """See :meth:`IRenderer.flush_wasm_state`. + + Local (wasm) backend override: pushes current Python VTK object + state to the wasm client via ``LocalView.update()``. + """ + self._local_view.update() + + # ------------------------------------------------------------------ + # IRenderer: frontend addressing + # ------------------------------------------------------------------ + + @property + def frontend_ref_name(self) -> str: + """See :meth:`IRenderer.frontend_ref_name`.""" + return self._local_view.ref_name + + # ------------------------------------------------------------------ + # Internal init helpers (moved verbatim from VisorScene) + # ------------------------------------------------------------------ + + def _initialize_vtk_renderer(self) -> vtkRenderer: + renderer = vtkRenderer() + renderer.SetBackground(VisorColors.BackgroundColor) + return renderer + + def _initialize_render_window(self) -> vtkRenderWindow: + render_window = vtkRenderWindow() + render_window.AddRenderer(self._vtk_renderer) + render_window.ShowWindowOff() + render_window.SetMultiSamples(0) + render_window.SetEnableTranslucentSurface(True) + return render_window + + def _initialize_render_window_interactor(self) -> vtkRenderWindowInteractor: + interactor = vtkRenderWindowInteractor() + interactor.SetRenderWindow(self._render_window) + interactor.SetInteractorStyle(vtkInteractorStyleTrackballCamera()) + # vtkCellPicker so GetCellId() is available on the WASM side. + cell_picker = vtkCellPicker() + cell_picker.SetTolerance(0.005) + interactor.SetPicker(cell_picker) + return interactor + + def _initialize_local_view(self) -> LocalView: + return LocalView( + self._render_window, + trame_server=self._server, + throttle_rate=4, + eager_sync=True, + listeners=("wasm_listeners", {}), + ) + + def _initialize_orientation_widget(self) -> VisorOrientationWidget: + widget = VisorOrientationWidget(self._vtk_renderer, self._render_window_interactor) + widget.register_with_local_view(self._local_view) + return widget + + def _initialize_cross_section_widget(self) -> VisorCrossSectionWidget: + widget = VisorCrossSectionWidget(self._render_window_interactor) + widget.register_with_local_view(self._local_view) + return widget + + def _initialize_bounding_box_widget(self) -> VisorBoundingBox: + widget = VisorBoundingBox(self._vtk_renderer.GetActiveCamera()) + widget.attach_to_renderer(self._vtk_renderer) + widget.register_with_local_view(self._local_view) + return widget + diff --git a/src/ansys/visor/viewer/renderer/null_renderer.py b/src/ansys/visor/viewer/renderer/null_renderer.py new file mode 100644 index 00000000..5bb35c7c --- /dev/null +++ b/src/ansys/visor/viewer/renderer/null_renderer.py @@ -0,0 +1,167 @@ +""" +NullRenderer +============ +Behaviour-free test double for :class:`IRenderer`. + +Implements every abstract method as a no-op so that the scene coordinator can +be unit-tested without a VTK environment. Methods whose return type is +annotated return the simplest valid empty value for that type; all others are +``pass``. No VTK imports, no local view, no side effects, no state. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ansys.visor.viewer.renderer.base import IRenderer + +if TYPE_CHECKING: + from vtkmodules.vtkCommonDataModel import vtkDataObject + + from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState + from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphPartNode + + +class NullRenderer(IRenderer): + """Null-object implementation of :class:`IRenderer` for use in tests.""" + + # ------------------------------------------------------------------ + # Wire contract + # ------------------------------------------------------------------ + + def build_renderer_annotation(self) -> None: + """See :meth:`IRenderer.build_renderer_annotation`. + + ``None``: a renderer with no client-side handles is represented by + wire absence, not by a value. + """ + return None + + # ------------------------------------------------------------------ + # Node lifecycle + # ------------------------------------------------------------------ + + def register_node( + self, node: "VisorSceneGraphPartNode", dataset: "vtkDataObject" + ) -> None: + pass + + def deregister_node(self, node_id: int) -> None: + pass + + def deregister_all(self) -> None: + pass + + # ------------------------------------------------------------------ + # Per-part visual mutations + # ------------------------------------------------------------------ + + def apply_visibility(self, node_id: int, visible: bool) -> None: + pass + + def apply_opacity(self, node_id: int, opacity: float) -> None: + pass + + def apply_diffuse_color(self, node_id: int, r: float, g: float, b: float) -> None: + pass + + def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: + pass + + def apply_selected(self, node_id: int, selected: bool, diffuse_rgb: list) -> None: + pass + + def apply_color_variable( + self, + node_id: int, + spectrum_id: str, + array_type: str, + array_name: str, + component: int, + min_val: float, + max_val: float, + ) -> None: + pass + + def clear_color_variable(self, node_id: int) -> None: + pass + + def refresh_color_variable_range( + self, + node_id: int, + spectrum_id: str, + array_type: str, + array_name: str, + component: int, + ) -> None: + pass + + # ------------------------------------------------------------------ + # Camera + # ------------------------------------------------------------------ + + def reset_camera(self, bounds: list[float]) -> None: + pass + + def get_camera_state(self) -> "VisorCameraState | None": + return None + + def sync_camera(self, camera_state: "VisorCameraState") -> None: + pass + + # ------------------------------------------------------------------ + # Widget control (cross-section, bounding box) + # ------------------------------------------------------------------ + + def set_cross_section_visibility(self, visible: bool) -> None: + pass + + def sync_cross_section_plane( + self, origin: list[float], normal: list[float] + ) -> None: + pass + + def set_bounding_box_visibility(self, visible: bool) -> None: + pass + + # ------------------------------------------------------------------ + # Widget fan-out + # ------------------------------------------------------------------ + + def update_bounds(self, bounds: list[float]) -> None: + pass + + def update_actor_count(self, count: int) -> None: + pass + + # ------------------------------------------------------------------ + # Picking + # ------------------------------------------------------------------ + + def pick_geometry( + self, + actor_wasm_id: int, + cell_id: int, + mode: str, + world_pos: tuple[float, float, float], + ) -> dict: + return {} + + # ------------------------------------------------------------------ + # Render / flush + # ------------------------------------------------------------------ + + def render(self) -> None: + pass + + def render_window_only(self) -> None: + pass + + # ------------------------------------------------------------------ + # Frontend addressing + # ------------------------------------------------------------------ + + @property + def frontend_ref_name(self) -> str: + return "" + diff --git a/src/ansys/visor/viewer/utils/__init__.py b/src/ansys/visor/viewer/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/utils/autosetup.py b/src/ansys/visor/viewer/utils/autosetup.py new file mode 100644 index 00000000..d05f599b --- /dev/null +++ b/src/ansys/visor/viewer/utils/autosetup.py @@ -0,0 +1,65 @@ +""" +Auto-setup script for preparing Visor WASM assets. + +This script downloads the required trame_vtklocal WASM content and +copies it into the frontend bundle. Intended for developers and CI builds. +""" + +import re +import shutil +import site +import sys +from importlib import metadata +from pathlib import Path + +import trame.app as trame_app +import trame_vtklocal.module as trame_vtklocal_module + + +def prepare_wasm_assets(project_root: Path) -> None: + """Download and copy trame_vtklocal WASM assets into the frontend.""" + + def get_vtk_version() -> str: + version = re.findall(r"\d+(?:\.\d+)*|$", metadata.version("vtk"))[0] + if not version: + raise Exception("Could not parse VTK version") + return version + + def download_wasm() -> None: + server = trame_app.get_server() + server.enable_module(trame_vtklocal_module) + + def copy_wasm(src: Path, dst: Path) -> None: + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) + + # --- execution --- + + version = get_vtk_version() + + download_wasm() + + if sys.platform == "win32": + site_packages = project_root / ".venv" / "Lib" / "site-packages" + elif sys.platform == "linux": + site_packages = Path(site.getsitepackages()[0]) + else: + raise RuntimeError(f"Unsupported platform: {sys.platform}") + + src = site_packages / "trame_vtklocal" / "module" / "serve" / "wasm" / version + + if not src.is_dir(): + raise Exception(f"WASM not found at: {src}") + + dst = project_root / "src" / "ansys" / "visor" / "visor-client" / "modules" / "wasm" + copy_wasm(src, dst) + + +def main(): + """Main function to be called when the script is executed directly.""" + prepare_wasm_assets(Path.cwd()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/ansys/visor/viewer/utils/perf_log_reader.py b/src/ansys/visor/viewer/utils/perf_log_reader.py new file mode 100644 index 00000000..03af0380 --- /dev/null +++ b/src/ansys/visor/viewer/utils/perf_log_reader.py @@ -0,0 +1,394 @@ +""" +perf_log_reader.py +------------------- +Lightweight helper used from a notebook to scrape the [PERF] lines that +the production code writes into visor.log after each profiled API call, +and save them as a self-describing JSON report. + +Supports multiple operation namespaces so the same reader can be used for +different API methods (e.g. ``update_variables`` or ``add_dataset``). +The ``namespace`` argument controls: + - which top-level [PERF] line is expected (for ``total_s`` validation) + - the output JSON filename prefix + +No reimplementation of the call chain โ€” this only reads the log. + +Usage in notebook (update_variables): + from ansys.visor.viewer.utils.perf_log_reader import PerfLogReader + + reader = PerfLogReader(report_dir="../../benchmark_results/update_variables") + + reader.mark() + vis.update_variables(dataset_id, [...]) + json_path = reader.save_report(dataset_label=Path(DATASET_FILE).name) + print(json_path.name) + +Usage in notebook (add_dataset): + reader = PerfLogReader( + report_dir="../../benchmark_results/add_dataset", + namespace="add_dataset", + ) + + reader.mark() + vis.add_dataset(DATASET_FILE) + json_path = reader.save_report(dataset_label=Path(DATASET_FILE).name) + print(json_path.name) +""" + +from __future__ import annotations + +import json +import os +import platform +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np + +from ansys.visor.viewer.config import settings + +# --------------------------------------------------------------------------- +# Regex patterns +# --------------------------------------------------------------------------- + +# Matches any [PERF] line, capturing: +# label โ€” everything after "[PERF] " up to the first " | " (or end of line) +# phases โ€” the rest of the line after the first " | " separator (may be empty) +_RE_PERF_LINE = re.compile( + r"\[PERF\]\s+(?P<label>[^|]+?)(?:\s+\|\s+(?P<phases>.+))?$" +) + +# Extracts all key=value or key=<float>s pairs from the phases segment. +# Matches: word_chars= followed by digits/dot with an optional trailing 's' +_RE_KV = re.compile(r"([\w.+\-']+)=([0-9.]+)s?") + +# --------------------------------------------------------------------------- +# Label โ†’ canonical phase-key mapping +# +# Keys in this dict are the *label* strings that PerfTimer writes +# (i.e. the text between "[PERF] " and the first " | "). +# Values map each phase name from the log to the key stored in report["phases"]. +# +# Adding a new phase to PerfTimer requires no regex changes โ€” only an entry +# here (and optionally in _PHASE_DISPLAY_LABELS below) if you want a +# human-readable label in the TXT report. +# --------------------------------------------------------------------------- + +# Each entry: log_label_prefix -> {phase_name_in_log: key_in_report_phases} +# Use a prefix match (startswith) so labels with variable content work, +# e.g. "_update_variable 'pressure'" matches "_update_variable". +_LABEL_PHASE_MAP: list[tuple[str, dict[str, str]]] = [ + ("update_variables ", { + "from_dict": "from_dict_total_s", + "update_variables_for_dataset": "update_variables_for_dataset_s", + "total": "total_s", + }), + ("VisorDataset.update_variables ", { + "validate": "validate_total_s", + "vtk_set_loop": "vtk_set_loop_total_s", + "reload": "reload_variables_total_s", + }), + ("update_variables_for_dataset ", { + "dataset_registry.update": "registry_update_s", + "update_descendant_parts": "update_descendant_parts_s", + "render": "render_scene_s", + }), + ("add_dataset", { + "resolve_io": "resolve_io_s", + "add_to_scene": "add_to_scene_s", + "finalize_scene": "finalize_scene_s", + "total": "total_s", + }), + ("render", { + "RenderWindow.Render": "render1_window_render_s", + "LocalView.update": "render1_local_view_update_s", + }), +] + +# --------------------------------------------------------------------------- +# Namespace โ†’ profile configuration +# +# Each entry describes how PerfLogReader behaves for a given operation +# namespace: +# file_prefix: prefix for the output JSON filename +# total_key: the phases dict key that must be present for a valid report +# --------------------------------------------------------------------------- +_NAMESPACE_CONFIG: dict[str, dict] = { + "update_variables": { + "file_prefix": "update_variables_profile", + "total_key": "total_s", + }, + "add_dataset": { + "file_prefix": "add_dataset_profile", + "total_key": "total_s", + }, +} + +def _match_label(label: str) -> dict[str, str] | None: + """Return the phase-key mapping for this label, or None if unrecognised.""" + for prefix, mapping in _LABEL_PHASE_MAP: + if label.startswith(prefix) or label.strip() == prefix.strip(): + return mapping + return None + + + + +def _get_environment() -> dict: + """Return a dict with versions of Python, VTK, NumPy, and OS platform info.""" + try: + from vtkmodules.vtkCommonCore import vtkVersion + vtk_ver = vtkVersion.GetVTKVersion() + except Exception: + vtk_ver = "unknown" + return { + "python": sys.version, + "vtk": vtk_ver, + "numpy": np.__version__, + "os": platform.platform(), + } + + +class PerfLogReader: + """ + Reads [PERF] timing lines from visor.log after each production call and + writes a per-run JSON report. + + Parameters + ---------- + report_dir: + Directory where JSON reports are written. + log_path: + Path to visor.log. Defaults to the standard location under + settings.default_log_dir. + target_s: + Performance target in seconds used for the meets_target flag. + namespace: + The operation being profiled. Controls the report filename prefix and + which phases key is used to validate that perf logging was active. + Supported values: ``"update_variables"`` (default) or ``"add_dataset"``. + """ + + def __init__( + self, + report_dir: str | os.PathLike = ".", + log_path: str | os.PathLike | None = None, + target_s: float = 0.1, + namespace: str = "update_variables", + ): + if namespace not in _NAMESPACE_CONFIG: + raise ValueError( + f"Unknown namespace {namespace!r}. " + f"Choose one of: {list(_NAMESPACE_CONFIG)}" + ) + self._report_dir = Path(report_dir) + self._report_dir.mkdir(parents=True, exist_ok=True) + self._log_path = Path(log_path) if log_path else Path(settings.default_log_dir) / "visor.log" + self._target_s = target_s + self._namespace = namespace + self._ns_cfg = _NAMESPACE_CONFIG[namespace] + self._mark_pos: int = 0 # byte offset in log file at last mark() + + def mark(self) -> None: + """ + Record the current end-of-file position in the log. + Call this immediately BEFORE the profiled API call + (e.g. ``vis.update_variables()`` or ``vis.add_dataset()``). + """ + if self._log_path.exists(): + self._mark_pos = self._log_path.stat().st_size + else: + self._mark_pos = 0 + + def save_report( + self, + dataset_label: str = "", + mesh_info: dict | None = None, + ) -> Path: + """ + Read the [PERF] lines appended since mark(), build a JSON report, + and write it to report_dir. + + Call this immediately AFTER the profiled API call (and after the + client-side updates have completed in the browser, if you want + client-side timings). + + Parameters + ---------- + dataset_label: + Filename of the dataset (e.g. 'vtk_object_sphere_r1028_v4_c3_z2.vtp'). + Stored in the report so the aggregator can reconstruct matrix axes. + mesh_info: + Optional dict with 'num_points' and 'num_cells'. If None, derived + from the per-variable records (n_tuples from _update_variable lines) + when available. + + Returns + ------- + Path + Path to the written JSON file. + """ + new_lines = self._read_new_lines() + report = self._parse_lines(new_lines) + + total_key = self._ns_cfg["total_key"] + if total_key not in report["phases"]: + raise RuntimeError( + f"No [PERF] lines for namespace '{self._namespace}' were found in the log " + "since the last mark(). " + "Perf logging may not be enabled โ€” add 'perf_logging: true' to " + "your .visor config file and restart the server before retrying." + ) + + self._build_report(report, dataset_label, mesh_info) + + ts = datetime.fromisoformat(report["timestamp"]).strftime("%Y%m%dT%H%M%S") + prefix = self._ns_cfg["file_prefix"] + json_path = self._report_dir / f"{prefix}_{ts}.json" + json_path.write_text(json.dumps(report, indent=2), encoding="utf-8") + return json_path + + def _build_report(self, report: dict, dataset_label: str, mesh_info: dict | None) -> None: + """Annotate a parsed report dict with metadata, mesh info, and derived fields.""" + report["dataset_label"] = dataset_label + report["namespace"] = self._namespace + report["timestamp"] = datetime.now(tz=timezone.utc).isoformat() + report["environment"] = _get_environment() + report["target_s"] = self._target_s + total_key = self._ns_cfg["total_key"] + report["total_s"] = report["phases"].get(total_key, 0.0) + report["meets_target"] = report["total_s"] <= self._target_s + + if mesh_info: + report["mesh_info"] = mesh_info + elif report["variables"]: + report["mesh_info"] = {"num_points": report["variables"][0]["n_tuples"]} + + report["num_points"] = ( + report["mesh_info"].get("num_points", 0) if report.get("mesh_info") else 0 + ) + report["total_elements"] = sum( + v["n_tuples"] * v["n_components"] for v in report["variables"] + ) + + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _read_new_lines(self) -> list[str]: + """Return only the lines appended to the log since mark().""" + if not self._log_path.exists(): + return [] + with open(self._log_path, encoding="utf-8", errors="replace") as f: + f.seek(self._mark_pos) + return f.read().splitlines() + + def _parse_lines(self, lines: list[str]) -> dict: + """ + Parse [PERF] lines into a structured report dict. + + Each line is dispatched to a focused _parse_* helper based on its + label. Adding or renaming a phase only requires updating + _LABEL_PHASE_MAP โ€” no per-line-type regex to touch here. + """ + phases: dict[str, float | int] = {} + variables: list[dict] = [] + client: dict = {} + + for line in lines: + m = _RE_PERF_LINE.search(line) + if not m: + continue + label = m.group("label").strip() + phases_str = m.group("phases") or "" + kv = {k: v for k, v in _RE_KV.findall(phases_str)} + self._parse_line(label, phases_str, kv, phases, variables, client) + + return { + "phases": phases, + "variables": variables, + "client": client, + "num_variables": int(phases.pop("n_vars", len(variables))), + } + + def _parse_line( + self, + label: str, + phases_str: str, + kv: dict[str, str], + phases: dict, + variables: list, + client: dict, + ) -> None: + """Dispatch one parsed [PERF] line to the appropriate handler.""" + if label == "client updateAsync": + client.update(self._parse_client_wasm_line(phases_str, kv)) + elif label == "client onServerUpdateAsync": + client.update(self._parse_client_server_update_line(kv)) + elif label.startswith("_update_variable "): + variables.append(self._parse_variable_line(label, kv)) + else: + self._parse_server_phase_line(label, kv, phases) + + @staticmethod + def _parse_client_wasm_line(phases_str: str, kv: dict[str, str]) -> dict: + """Parse the 'client updateAsync' [PERF] line from the browser.""" + result = { + "total_ms": float(kv.get("total", 0)), + "wasm_ms": float(kv.get("wasm", 0)), + "resize_ms": float(kv.get("resize", 0)), + "state_count": int(float(kv.get("states", 0))), + "blob_count": int(float(kv.get("blobs", 0))), + } + # MB values appear in the log line in a fixed order: + # states=N (X MB) | blobs=N (Y MB total, largest=Z MB in ...ms) + mb_keys = ("state_total_mb", "blob_total_mb", "blob_max_mb") + mb_vals = re.findall(r"([\d.]+)\s*MB", phases_str) + for key, val in zip(mb_keys, mb_vals): + result[key] = float(val) + dur_m = re.search(r"in\s+([\d.]+)ms", phases_str) + result["blob_max_dur_ms"] = float(dur_m.group(1)) if dur_m else 0.0 + return result + + @staticmethod + def _parse_client_server_update_line(kv: dict[str, str]) -> dict: + """Parse the 'client onServerUpdateAsync' [PERF] line from the browser.""" + return {"handler_ms": float(kv.get("handler", 0))} + + @staticmethod + def _parse_variable_line(label: str, kv: dict[str, str]) -> dict: + """Parse a per-variable '_update_variable ...' [PERF] line.""" + label_kv = {k: v for k, v in _RE_KV.findall(label)} + name_m = re.search(r"'([^']+)'", label) + name = name_m.group(1) if name_m else label + n_tuples = int(label_kv.get("n_tuples", 0)) + n_components = int(label_kv.get("n_components", 1)) + return { + "name": name, + "n_tuples": n_tuples, + "n_components": n_components, + "set_loop_s": float(kv.get("set_loop+modified", 0.0)), + "total_elements": n_tuples * n_components, + } + + @staticmethod + def _parse_server_phase_line(label: str, kv: dict[str, str], phases: dict) -> None: + """Parse a standard server-side [PERF] phase line into the phases dict.""" + phase_map = _match_label(label) + if phase_map is None: + return # unrecognised label โ€” ignore silently + for log_key, report_key in phase_map.items(): + if log_key in kv: + phases[report_key] = float(kv[log_key]) + # n_vars is embedded in the label of the top-level update_variables lines + if label.startswith("update_variables ") or label.startswith("VisorDataset.update_variables "): + label_kv = {k: v for k, v in _RE_KV.findall(label)} + if "n_vars" in label_kv: + phases["n_vars"] = int(label_kv["n_vars"]) + + + + diff --git a/src/ansys/visor/viewer/vtk/__init__.py b/src/ansys/visor/viewer/vtk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/vtk/datasets/__init__.py b/src/ansys/visor/viewer/vtk/datasets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/vtk/datasets/part_index.py b/src/ansys/visor/viewer/vtk/datasets/part_index.py new file mode 100644 index 00000000..b6b213ba --- /dev/null +++ b/src/ansys/visor/viewer/vtk/datasets/part_index.py @@ -0,0 +1,152 @@ +""" +PartIndex: owns stable runtime part-ID assignment for a dataset. + +Built once from a vtkDataObject at add_dataset time. IDs are seeded from +the scene-graph node IDs (same JavaScript-safe integer convention) so that +part_id == scene-graph node ID -- the contract the frontend relies on to +apply per-part state (opacity, visibility, color) to the right VTK actor. +If no seed is provided for a given name, a fresh random ID is generated as +a fallback. IDs are session-scoped -- they do not survive a server restart. +The name <-> id bridge exists so that VisorStateMapper can translate between +the runtime layer (IDs) and the persistence layer (names) for +save_state / load_state. +""" +from __future__ import annotations + +from dataclasses import dataclass + +from vtkmodules.vtkCommonDataModel import vtkCompositeDataSet, vtkDataObject + +from ansys.visor.viewer.core.visor_helpers import get_random_javascript_safe_id, is_composite_dataset +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +logger = VisorDefaultLogger(__name__) + + +@dataclass +class PartEntry: + """Metadata for a single leaf part inside a dataset.""" + part_id: int # stable runtime ID, assigned once at load + name: str # block name from VTK metadata (or fallback) + flat_index: int # ordinal position in the SkipEmptyNodes composite iterator + + +class PartIndex: + """ + Builds and owns stable runtime part IDs for a vtkDataObject. + + For non-composite datasets there is exactly one implicit part. + For composite datasets there is one entry per non-empty leaf block, + traversed with SkipEmptyNodesOn() so flat_index values are contiguous. + + Contract + -------- + - part_id values are assigned once and never change for the lifetime + of the dataset. + - Part names must be unique within a dataset for save_state / load_state + round-trips to be reliable (the same constraint already applies to + dataset names in the registry). + - Part IDs are NOT stable across server restarts. + """ + + def __init__(self, data: vtkDataObject, dataset_name: str = "", seed_ids: dict[str, int] | None = None): + self._parts: dict[int, PartEntry] = {} # part_id -> entry + self._name_to_id: dict[str, int] = {} + self._seed_ids: dict[str, int] = seed_ids or {} + self._build(data, dataset_name) + + # ------------------------------------------------------------------ + # Build + # ------------------------------------------------------------------ + + def _build(self, data: vtkDataObject, dataset_name: str) -> None: + """Build the part IDs and names.""" + if not is_composite_dataset(data): + self._add_entry(name=dataset_name, flat_index=0) + else: + composite = data # type: vtkCompositeDataSet + it = composite.NewIterator() + it.SkipEmptyNodesOn() + flat_idx = 0 + while not it.IsDoneWithTraversal(): + meta = it.GetCurrentMetaData() + name_key = vtkCompositeDataSet.NAME() + name = ( + meta.Get(name_key) + if (meta is not None and meta.Has(name_key)) + else "untitled" + ) + if name in self._name_to_id: + logger.warning( + f"Duplicate part name '{name}' detected in dataset. " + f"The nameโ†’id map will only retain the last part with this name, " + f"which means save_state/load_state will be unreliable for these parts. " + f"Part IDs remain unique and all runtime operations are unaffected." + ) + self._add_entry(name=name, flat_index=flat_idx) + flat_idx += 1 + it.GoToNextItem() + + def _add_entry(self, name: str, flat_index: int) -> PartEntry: + """Add a new entry to the part IDs and names.""" + pid = self._seed_ids.get(name) or get_random_javascript_safe_id() + entry = PartEntry(part_id=pid, name=name, flat_index=flat_index) + self._parts[pid] = entry + self._name_to_id[name] = pid # last writer wins on collision + return entry + + + # ------------------------------------------------------------------ + # Lookup + # ------------------------------------------------------------------ + + def get_entry(self, part_id: int) -> PartEntry | None: + """Get the entry corresponding to the given part_id.""" + return self._parts.get(part_id) + + def get_leaf_block(self, part_id: int, data: vtkDataObject) -> vtkDataObject | None: + """ + Return the leaf vtkDataObject corresponding to *part_id*. + + For a non-composite dataset the data object itself is returned. + For composite datasets the iterator is advanced to the correct + flat_index (SkipEmptyNodesOn, matching build order). + """ + entry = self._parts.get(part_id) + if entry is None: + logger.warning(f"part_id {part_id} not found in PartIndex.") + return None + + if not is_composite_dataset(data): + return data + + composite = data # type: vtkCompositeDataSet + it = composite.NewIterator() + it.SkipEmptyNodesOn() + for _ in range(entry.flat_index): + it.GoToNextItem() + return it.GetCurrentDataObject() + + # ------------------------------------------------------------------ + # Maps (used by VisorStateMapper for runtime <-> persisted translation) + # ------------------------------------------------------------------ + + @property + def name_to_id_map(self) -> dict[str, int]: + """Return a dictionary that maps part IDs to ids.""" + return dict(self._name_to_id) + + @property + def id_to_name_map(self) -> dict[int, str]: + """Return a dictionary that maps part IDs to names.""" + return {e.part_id: e.name for e in self._parts.values()} + + @property + def part_ids(self) -> list[int]: + """Return a list of all part IDs.""" + return list(self._parts.keys()) + + def list_parts(self) -> list[dict]: + """Return a list containing each part's ID and name.""" + return [{"part_id": e.part_id, "name": e.name} for e in self._parts.values()] + diff --git a/src/ansys/visor/viewer/vtk/datasets/visor_dataset.py b/src/ansys/visor/viewer/vtk/datasets/visor_dataset.py new file mode 100644 index 00000000..8ba4b8d3 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/datasets/visor_dataset.py @@ -0,0 +1,301 @@ +"""Class representing a Visor dataset.""" + +import copy +from typing import Dict, List + +import numpy as np +from vtkmodules.util.numpy_support import vtk_to_numpy +from vtkmodules.vtkCommonDataModel import vtkDataObject + +from ansys.visor.viewer.core.metadata import ExtendedMetadata +from ansys.visor.viewer.core.perf_timer import PerfTimer +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType +from ansys.visor.viewer.core.visor_helpers import is_composite_dataset +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.models.common.part_properties import PartProperties +from ansys.visor.viewer.models.info.visor_dataset_info import VisorDatasetInfo +from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState +from ansys.visor.viewer.vtk.datasets.part_index import PartIndex +from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables +from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate +from ansys.visor.viewer.vtk.variables.visor_variables import VisorVariables + +"""Logging configuration""" +logger = VisorDefaultLogger(__name__) + + +class VisorDataset: + """Base class for Visor datasets.""" + + def __init__(self, id: int, name: str, data: vtkDataObject, part_name_to_id: Dict[str, int], metadata: ExtendedMetadata): + self.id: int = id + self.data: vtkDataObject = data + + # PartIndex is the single source of truth for part topology and IDs. + # It is built directly from the VTK data object โ€” no scene graph required. + # seed_ids pins part IDs to the scene-graph node IDs so the frontend can + # correlate part_states entries with VTK actor properties. + self.part_index: PartIndex = PartIndex(data, name, seed_ids=part_name_to_id) + + # Get the dataset info from metadata + self.info: VisorDatasetInfo = self._build_dataset_info(id, name, metadata) + # Build the initial runtime dataset state from persisted part names -> IDs via PartIndex + self.state: RuntimeDatasetState = self.persisted_to_runtime_state(metadata.state.parts) + + # Per-part variable metadata, keyed by part_id. + # Works for both composite and non-composite datasets. + self._part_variables: Dict[int, VisorVariables] = self._load_variables(data) + + # Every newly loaded dataset needs its snapshot written at least once. + self.is_dirty: bool = True + + + @property + def name(self) -> str: + """Return the name of the dataset.""" + return self.info.name + + @property + def info_dict(self) -> dict: + """Return the dataset info dictionary.""" + return self.info.model_dump() + + def is_composite(self) -> bool: + """Return whether the dataset is a composite dataset.""" + return is_composite_dataset(self.data) + + # ------------------------------------------------------------------ + # Variable operations + # ------------------------------------------------------------------ + + def list_variables(self) -> List[VisorPartVariables]: + """ + List all variables in the dataset, grouped by part. + + Returns a list of VisorPartVariables โ€” one entry per part. + For non-composite datasets this is a single-element list. + """ + result = [] + for part_id, part_vars in self._part_variables.items(): + entry = self.part_index.get_entry(part_id) + result.append(VisorPartVariables( + part_id=part_id, + part_name=entry.name if entry else "", + variables=part_vars.list(), + )) + return result + + def set_state(self, new_state: PersistedDatasetState) -> None: + """Set the state of the dataset.""" + self.state = self.persisted_to_runtime_state(new_state.parts) + + def mark_clean(self) -> None: + """Clear the dirty flag after a successful snapshot write.""" + self.is_dirty = False + + def update_variables(self, variable_list: List[VisorVariableUpdate]) -> None: + """ + Update variables in the dataset with new data. + + For composite (multiblock) datasets each VisorVariableUpdate must + carry a ``part_id`` identifying which leaf block to target. + For non-composite datasets ``part_id`` is optional and ignored. + """ + timer = PerfTimer("VisorDataset.update_variables", logger, n_vars=len(variable_list)) + + if self.is_composite(): + self._update_variables_composite(variable_list, timer) + else: + self._update_variables_simple(variable_list, timer) + + self.is_dirty = True + timer.log() + + # ------------------------------------------------------------------ + # Internal update helpers + # ------------------------------------------------------------------ + + def _update_variables_simple( + self, + variable_list: List[VisorVariableUpdate], + timer: PerfTimer, + ) -> None: + """Update path for a non-composite (single-block) dataset.""" + part_ids = self.part_index.part_ids + if not part_ids: + raise RuntimeError("PartIndex is empty for a non-composite dataset.") + part_vars = self._part_variables[part_ids[0]] + + with timer.phase("validate"): + if not part_vars.validate_new_variables(variable_list): + raise ValueError("One or more variables are invalid and cannot be updated.") + + with timer.phase("vtk_set_loop"): + for new_variable in variable_list: + self._update_variable(new_variable, self.data) + + with timer.phase("reload"): + part_vars.reload(self.data) + + def _update_variables_composite( + self, + variable_list: List[VisorVariableUpdate], + timer: PerfTimer, + ) -> None: + """ + Update path for a composite (multiblock) dataset. + + Each update may either target a specific part (``part_id`` set) or be + broadcast to all parts (``part_id=None``). A broadcast update is + applied to every part for which the variable exists and the data length + matches; parts that do not satisfy both conditions are skipped with a + warning rather than raising, so that heterogeneous multiblock datasets + where not every block carries every variable are handled gracefully. + """ + targeted = [v for v in variable_list if v.part_id is not None] + broadcast = [v for v in variable_list if v.part_id is None] + + # Build per-part update lists, starting from targeted updates. + by_part: Dict[int, List[VisorVariableUpdate]] = {} + for var in targeted: + by_part.setdefault(var.part_id, []).append(var) + + # Expand broadcast updates: add to every part where validation passes. + if broadcast: + for part_id in self.part_index.part_ids: + part_vars = self._part_variables.get(part_id) + if part_vars is None: + continue + accepted = [v for v in broadcast if part_vars.validate_new_variables([v])] + skipped = [v.name for v in broadcast if not part_vars.validate_new_variables([v])] + if skipped: + logger.warning( + f"Broadcast update skipped variable(s) {skipped} on part_id {part_id} " + f"(variable not found or data shape mismatch)." + ) + if accepted: + by_part.setdefault(part_id, []).extend(accepted) + + with timer.phase("validate"): + for part_id, part_vars_list in by_part.items(): + part_vars = self._part_variables.get(part_id) + if part_vars is None: + raise ValueError(f"part_id {part_id} not found in dataset {self.id}.") + if not part_vars.validate_new_variables(part_vars_list): + raise ValueError( + f"One or more variables are invalid for part_id {part_id}." + ) + + with timer.phase("vtk_set_loop"): + for part_id, part_vars_list in by_part.items(): + leaf = self.part_index.get_leaf_block(part_id, self.data) + for new_variable in part_vars_list: + self._update_variable(new_variable, leaf) + + with timer.phase("reload"): + for part_id in by_part: + leaf = self.part_index.get_leaf_block(part_id, self.data) + self._part_variables[part_id].reload(leaf) + + def _update_variable( + self, + new_variable: VisorVariableUpdate, + dataset: VisorDatasetType, + ) -> None: + """ + Write new data into a single variable array on *dataset* in-place. + Uses zero-copy numpy view + np.copyto for C-level memcpy speed + while preserving VTK array object identity. + """ + data = self._get_data_for_type(dataset, new_variable.type) + array = data.GetArray(new_variable.name) + + if array is None: + raise ValueError(f"Variable '{new_variable.name}' not found in dataset.") + if array.GetNumberOfTuples() != len(new_variable.data): + raise ValueError( + f"Data length mismatch for variable '{new_variable.name}': " + f"expected {array.GetNumberOfTuples()}, got {len(new_variable.data)}." + ) + + timer = PerfTimer( + f"_update_variable '{new_variable.name}'", logger, + n_tuples=array.GetNumberOfTuples(), + n_components=new_variable.num_components, + ) + with timer.phase("set_loop+modified"): + buf = vtk_to_numpy(array) + np.copyto(buf.ravel(), new_variable.data.ravel(), casting="unsafe") + array.Modified() + timer.log() + + def _get_data_for_type(self, dataset: VisorDatasetType, var_type: VisorVtkVariableType): + if var_type == VisorVtkVariableType.POINT: + return dataset.GetPointData() + elif var_type == VisorVtkVariableType.CELL: + return dataset.GetCellData() + else: + raise ValueError(f"Unsupported variable type: {var_type}") + + def _load_variables(self, data: vtkDataObject) -> Dict[int, VisorVariables]: + """ + Build per-part VisorVariables from the VTK data object. + For non-composite datasets there is a single entry. + For composite datasets there is one entry per non-empty leaf block. + """ + result: Dict[int, VisorVariables] = {} + for part_id in self.part_index.part_ids: + leaf = self.part_index.get_leaf_block(part_id, data) + if leaf is not None: + result[part_id] = VisorVariables(leaf) + return result + + # ------------------------------------------------------------------ + # Persistence helpers + # ------------------------------------------------------------------ + + @staticmethod + def _build_dataset_info( + dataset_id: int, + dataset_name: str, + metadata: ExtendedMetadata, + ) -> VisorDatasetInfo: + """Construct a VisorDatasetInfo from the dataset ID, name, and ExtendedMetadata.""" + return VisorDatasetInfo( + id=dataset_id, + name=dataset_name, + unit=metadata.unit, + file_path=metadata.file_path, + metadata_path=metadata.metadata_path, + ) + + def persisted_to_runtime_state( + self, + parts_by_name: dict[str, PartProperties], + ) -> RuntimeDatasetState: + """Convert persisted part states (keyed by part name) to runtime part states (keyed by part ID).""" + name_to_id = self.part_index.name_to_id_map + part_states = {} + for part_name, part_properties in parts_by_name.items(): + part_id = name_to_id.get(part_name) + if part_id is not None: + part_states[part_id] = copy.deepcopy(part_properties) + return RuntimeDatasetState.from_components(id=self.id, part_states=part_states) + + def runtime_to_persisted_state(self, runtime_state: RuntimeDatasetState) -> PersistedDatasetState: + """Convert runtime state to persisted state (keyed by run ID).""" + id_to_name = self.part_index.id_to_name_map + part_states = {} + for part_id, runtime_properties in runtime_state.part_states.items(): + part_name = id_to_name.get(part_id) + if part_name is None: + continue + part_states[part_name] = runtime_properties.to_part_properties() + return PersistedDatasetState( + source_file_path=getattr(self.info, "file_path", None), + source_metadata_path=getattr(self.info, "metadata_path", None), + parts=part_states, + ) + diff --git a/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py b/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py new file mode 100644 index 00000000..0b7e4d90 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py @@ -0,0 +1,221 @@ +"""Registry for managing multiple Visor datasets within a scene.""" + +import re +from typing import Dict, List + +from ansys.visor.viewer.core.metadata import ExtendedMetadata +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState +from ansys.visor.viewer.vtk.datasets.visor_dataset import VisorDataset +from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables +from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate + +logger = VisorDefaultLogger(__name__) + + + +class VisorDatasetRegistry: + """ + Registry for managing multiple Visor datasets within a scene. + + It allows adding, removing, and retrieving datasets, as well as managing their states and variables + collectively. + + Attributes: + datasets (Dict[int, VisorDataset]): A dictionary mapping dataset IDs to VisorDataset instances. + unit (str): The unit associated with the datasets in the registry. + """ + def __init__(self): + """Initialize the registry.""" + self.datasets: Dict[int, VisorDataset] = {} + self.unit: str = "" + + @property + def runtime_state_dict(self) -> Dict[int, RuntimeDatasetState]: + """ + Get the combined state of all datasets in the registry. + + Returns: + Dict[str, Dict[int, PartProperties]]: A dict with a single key 'parts' mapping + to a flat dict of all parts across all dataset states. + + Note: + The structure is flat to mirror the current JSON schema used by the frontend. + """ + return {id:dataset.state for id, dataset in self.datasets.items()} + + @property + def count(self) -> int: + """ + Get the number of datasets in the registry. + + Returns: + int: The count of datasets. + """ + return len(self.datasets) + + def list_info(self) -> Dict[int, dict]: + """ + Get the state information for all datasets in the registry. + + Returns a dictionary mapping dataset IDs to their respective state information. + """ + return {k: dataset.info_dict for k, dataset in self.datasets.items()} + + def get_by_name(self, name: str) -> VisorDataset | None: + """ + Retrieve a dataset by its name. + + Args: + name (str): The name of the dataset to retrieve. + + Returns: + VisorDataset | None: The dataset with the specified name, or None if not found. + """ + for dataset in self.datasets.values(): + if dataset.name == name: + return dataset + return None + + def add(self, + dataset_id: int, + dataset_name: str, + input: VisorDatasetType, + part_name_to_id: Dict[str, int], + metadata: ExtendedMetadata, + ) -> VisorDataset: + """ + Add a new dataset to the registry with associated metadata. + """ + self._update_unit(metadata) + + # VisorDataset builds its own PartIndex directly from the VTK data object, + # seeded with the scene-graph node IDs so part_id == scene-graph node ID. + dataset = VisorDataset(dataset_id, dataset_name, input, part_name_to_id, metadata) + + # Store the dataset in the registry + self.datasets[dataset_id] = dataset + + return dataset + + def remove(self, dataset_id: int) -> None: + """ + Remove a dataset from the registry by its ID. + """ + if dataset_id in self.datasets: + self.datasets.pop(dataset_id) + + def _get_unique_dataset_name(self, name: str) -> str: + """ + Rename the dataset by appending a suffix to ensure uniqueness. + + @return + A unique dataset name. If the original name is unique, it is returned as is. + """ + # Get existing dataset names + dataset_names = [dataset.name for dataset in self.datasets.values()] + + # If the name is unique, return it as is + if name not in dataset_names: + return name + + # Extract base name and suffix if present + match = re.match(r"^(.*?)(?:_(\d+))?$", name) + base_name = match.group(1) + suffix = int(match.group(2)) if match.group(2) else 1 + + # Generate new name with incremented suffix until unique + new_name = f"{base_name}_{suffix}" + while new_name in dataset_names: + suffix += 1 + new_name = f"{base_name}_{suffix}" + return new_name + + def get_sanitized_metadata_name(self, metadata: ExtendedMetadata) -> str: + """ + Normalize the metadata by ensuring unique dataset names. + """ + # TODO: move to protected after composite dataset support is added + # and Scene.add_dataset delegates more cleanly to DatasetRegistry + + if metadata is None: + raise ValueError("Metadata cannot be None.") + + new_name = self._get_unique_dataset_name(metadata.name) + if new_name != metadata.name: + logger.warning(f"Dataset name '{metadata.name}' already exists. Renaming to '{new_name}'.") + + return new_name + + def clear(self): + """ + Clear all datasets from the registry. + """ + self.datasets.clear() + self._reset_unit() + + def list_variables(self, dataset_id: int) -> List[VisorPartVariables]: + """ + Get the variable information from the scene graph. + """ + dataset = self.datasets.get(dataset_id) + if dataset is None: + msg = f"Dataset with id {dataset_id} not found." + logger.error(msg) + raise ValueError(msg) + return dataset.list_variables() + + def update_variables(self, dataset_id: int, variables: List[VisorVariableUpdate]) -> None: + """ + Update the variables in the scene graph. + """ + dataset = self.datasets.get(dataset_id) + if dataset is None: + msg = f"Dataset with id {dataset_id} not found." + logger.error(msg) + raise ValueError(msg) + dataset.update_variables(variables) + + def _validate_unit(self, metadata: ExtendedMetadata) -> bool: + """ + Validate that the unit of the new metadata matches the existing unit. + + @return + True if the units match or if the existing unit is empty, False otherwise. + """ + # TODO: move to protected after composite dataset support is added + # and Scene.add_dataset delegates more cleanly to DatasetRegistry + + if metadata is None: + return False + + if len(self.datasets) == 0: + # if this is the first dataset being added into a blank scene, accept any unit + return True + + if metadata.unit != self.unit: + msg = (f"Unit mismatch: existing ('{self.unit}') vs new ('{metadata.unit}')." + f" Setting unit to null.") + logger.warning(msg) + return False + + return True + + def _update_unit(self, metadata: ExtendedMetadata): + """ + Update the unit to the specified value. + """ + if self._validate_unit(metadata): + # store unit to this class + self.unit = metadata.unit + else: + # reset unit to empty string + self._reset_unit() + + def _reset_unit(self): + """ + Reset the unit to an empty string. + """ + self.unit = "" + diff --git a/src/ansys/visor/viewer/vtk/io/__init__.py b/src/ansys/visor/viewer/vtk/io/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/vtk/io/dataset_to_file.py b/src/ansys/visor/viewer/vtk/io/dataset_to_file.py new file mode 100644 index 00000000..a37f8188 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/io/dataset_to_file.py @@ -0,0 +1,35 @@ +"""Helper function to write a VTK dataset to a file.""" + +from pathlib import Path + +from vtkmodules.vtkCommonDataModel import vtkDataObject +from vtkmodules.vtkIOHDF import vtkHDFWriter + +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +logger = VisorDefaultLogger(__name__) + + +def dataset_to_file(path: str | Path, dataset: vtkDataObject) -> Path: + """Write *dataset* to disk as a VTKHDF file at *path*. + + The parent directory is created if it does not already exist. + + Args: + path: Destination file path (should end with ``.vtkhdf``). + dataset: The VTK data object to serialise. + + Returns: + The :class:`pathlib.Path` of the file that was written. + """ + dest = Path(path) + dest.parent.mkdir(parents=True, exist_ok=True) + + writer = vtkHDFWriter() + writer.SetFileName(str(dest)) + writer.SetInputData(dataset) + writer.Write() + + + logger.debug(f"Dataset written to {dest}") + return dest diff --git a/src/ansys/visor/viewer/vtk/io/file_to_dataset.py b/src/ansys/visor/viewer/vtk/io/file_to_dataset.py new file mode 100644 index 00000000..a8d94fd5 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/io/file_to_dataset.py @@ -0,0 +1,78 @@ +"""Helper function to read a VTK dataset from a file and return it as a vtkDataSet or vtkCompositeDataSet.""" + +import os + +from vtkmodules.vtkCommonDataModel import vtkCompositeDataSet, vtkDataObject, vtkDataSet +from vtkmodules.vtkIOHDF import vtkHDFReader +from vtkmodules.vtkIOXML import ( + vtkXMLMultiBlockDataReader, + vtkXMLPolyDataReader, + vtkXMLReader, + vtkXMLUnstructuredGridReader, +) + +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +"""Logging configuration""" +logger = VisorDefaultLogger(__name__) + + +def file_to_dataset(file_path: str) -> vtkDataSet | vtkCompositeDataSet: + """""" + try: + logger.debug(f"Beginning creating dataset from file: {file_path}") + if not os.path.exists(file_path): + msg = f"file does not exist: {file_path}" + logger.error(msg) + raise RuntimeError(msg) + # make file_path lowercase so extension testing is case-insensitive + filename: str = os.path.basename(file_path).lower() + extension: str = os.path.splitext(filename)[1][1:] + reader: vtkXMLReader | vtkHDFReader + is_hdf = False + if extension == "vtu": + """""" + logger.debug(f"vtkUnstructuredGrid detected: {file_path}") + reader = vtkXMLUnstructuredGridReader() + reader.SetFileName(file_path) + elif extension == "vtp": + """""" + logger.debug(f"vtkPolyData detected: {file_path}") + reader = vtkXMLPolyDataReader() + reader.SetFileName(file_path) + elif extension == "vtm": + """""" + logger.debug(f"vtkMultiBlockDataSet detected: {file_path}") + reader = vtkXMLMultiBlockDataReader() + reader.SetFileName(file_path) + elif extension == "vtkhdf": + """""" + logger.debug(f"VTKHDF dataset detected: {file_path}") + reader = vtkHDFReader() + reader.SetFileName(file_path) + is_hdf = True + else: + """""" + msg = f"Unsupported file: {file_path}" + logger.error(msg) + raise RuntimeError(msg) + reader.Update() + output: vtkDataSet | vtkCompositeDataSet = reader.GetOutput() + if is_hdf and not isinstance(output, (vtkDataSet, vtkCompositeDataSet)): + msg = f"VTKHDF file contains an unsupported dataset type ({type(output).__name__}): {file_path}" + logger.error(msg) + raise RuntimeError(msg) + + # Deep-copy so we own the data independently of the reader. + copy: vtkDataObject = output.NewInstance() + copy.DeepCopy(output) + + # Release the reader and its file handle + del reader + + logger.debug(f"Finished creating dataset from file: {file_path}") + return copy + except RuntimeError as e: + msg = f"could not create dataset from file: {e}" + logger.error(msg) + raise RuntimeError(msg) diff --git a/src/ansys/visor/viewer/vtk/io/visor_file_io.py b/src/ansys/visor/viewer/vtk/io/visor_file_io.py new file mode 100644 index 00000000..80f6d19c --- /dev/null +++ b/src/ansys/visor/viewer/vtk/io/visor_file_io.py @@ -0,0 +1,348 @@ +"""Filesystem I/O helpers for Visor. + +:class:`VisorFileIO` owns every interaction between the application and the +filesystem. Keeping this logic here means +:class:`~ansys.visor.viewer.app.visor_vtk.VisorVTK` +never needs to know about ``Path``, ``open``, or JSON serialisation details. + +Responsibilities (current scope): + +General file reading (used at start / add_dataset time): +- Reading a VTK dataset from a file path on disk. +- Reading a metadata JSON file from disk into a + :class:`~ansys.visor.viewer.core.metadata.Metadata` object. + +Save/load state: +- Resolving the canonical ``visor.json`` path inside a save directory. +- Writing a :class:`~ansys.visor.viewer.models.persist.persisted_viewer_state.PersistedViewerStateV1` + to ``visor.json``. +- Reading and parsing ``visor.json`` back into a + :class:`~ansys.visor.viewer.models.persist.persisted_viewer_state.PersistedViewerStateV1`. +- Writing in-memory ``vtkDataObject`` instances to disk as VTKHDF snapshots. +""" + +import json +from pathlib import Path + +from ansys.visor.viewer.core.metadata import ExtendedMetadata, Metadata +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState +from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 +from ansys.visor.viewer.vtk.io.dataset_to_file import dataset_to_file +from ansys.visor.viewer.vtk.io.file_to_dataset import file_to_dataset + +logger = VisorDefaultLogger(__name__) + +_STATE_FILE_NAME = "visor.json" +_DATASET_SNAPSHOT_NAME_SUFFIX = "_snapshot" +_DATASET_SNAPSHOT_EXTENSION = "vtkhdf" + + +def _try_unlink(path: Path) -> bool: + """Delete *path*, returning ``True`` on success. + + On Windows, ``vtkHDFReader`` registers multiblock HDF5 files in a global + HDF5 C-library context that cannot be released from Python. If the file + is still locked a ``PermissionError`` is caught, a warning is logged, and + ``False`` is returned so the caller can decide how to proceed. + """ + try: + path.unlink() + return True + except PermissionError: + logger.warning( + f"Could not delete stale snapshot '{path}': file is locked " + f"(likely by VTK's HDF5 reader). It will be removed on the next save." + ) + return False + + +class VisorFileIO: + """Handles all filesystem I/O for Visor. + + This class is stateless โ€” all methods are independent of one another and + require only their explicit arguments. There is no need to instantiate it + with any configuration; a single instance can be shared or a new one + created per operation. + """ + + # ------------------------------------------------------------------ + # General file reading (used at start / add_dataset time) + # ------------------------------------------------------------------ + + def read_dataset(self, path: str) -> VisorDatasetType: + """Load a VTK dataset from *path* and return the ``vtkDataObject``. + + Args: + path: Absolute or relative path to a ``.vtu``, ``.vtp``, ``.vtm``, + or ``.vtkhdf`` file. + + Returns: + The loaded VTK dataset object. + """ + return file_to_dataset(path) + + def read_snapshot(self, path: str) -> VisorDatasetType: + """Load a VTKHDF snapshot from *path*, keeping the original file unlocked. + + Identical to :meth:`read_dataset` except that VTKHDF files are read via + a temporary copy so the HDF5 global lock (which cannot be released from + Python) lands on the disposable copy rather than the original. Use this + when reading from ``save_dir`` so that ``save_state`` can later overwrite + or delete the file without hitting a Windows file-lock error. + + Args: + path: Absolute or relative path to a ``.vtkhdf`` snapshot file. + + Returns: + The loaded VTK dataset object. + """ + return file_to_dataset(path) + + def read_metadata(self, path: str) -> Metadata: + """Parse a metadata JSON file at *path* and return a :class:`Metadata` object. + + Args: + path: Absolute or relative path to a JSON file whose contents + match the :class:`~ansys.visor.viewer.core.metadata.Metadata` schema. + + Returns: + The deserialised :class:`Metadata` instance. + """ + with open(path, "rb") as fh: + metadata_dict = json.load(fh) + return Metadata(**metadata_dict) + + def resolve( + self, + input: str | VisorDatasetType | None, + metadata: str | Metadata | None, + ) -> tuple[VisorDatasetType | None, ExtendedMetadata]: + """Resolve a raw user-supplied *input* and *metadata* into concrete typed values. + + Accepts any combination the public API allows โ€” file paths, VTK objects, + ``Metadata`` instances, or ``None`` โ€” and returns a normalised pair ready + for :meth:`~ansys.visor.viewer.vtk.scene.base.VisorSceneBase.add_dataset`. + + Args: + input: A file path string, a ``vtkDataObject``, or ``None``. + metadata: A file path string, a :class:`Metadata` instance, or ``None``. + + Returns: + A ``(dataset, extended_metadata)`` tuple. *dataset* is ``None`` when + *input* is ``None``. + """ + loaded_input = self.resolve_input(input) + loaded_metadata = self.resolve_metadata(metadata, input) + + extended_metadata = ExtendedMetadata(**loaded_metadata.model_dump()) + if isinstance(input, str): + extended_metadata.file_path = input + if isinstance(metadata, str): + extended_metadata.metadata_path = metadata + + return loaded_input, extended_metadata + + def resolve_input(self, input: str | VisorDatasetType | None) -> VisorDatasetType | None: + """Resolve *input* to a ``vtkDataObject``, reading from disk if it is a path. + + Args: + input: A file path string, a ``vtkDataObject``, or ``None``. + + Returns: + The resolved dataset, or ``None`` if *input* is ``None``. + """ + if input is None: + return None + + if isinstance(input, VisorDatasetType): + return input + + if not isinstance(input, str): + raise TypeError(f"Unsupported input type: {type(input).__name__}") + + return self.read_dataset(input) + + def resolve_metadata( + self, + metadata: str | Metadata | None, + input: str | VisorDatasetType | None + ) -> Metadata: + """Resolve *metadata* to a :class:`Metadata` instance, reading from disk if it is a path. + + Returns a default ``Metadata(name="model", unit="")`` when *metadata* is ``None``. + + Args: + metadata: A file path string, a :class:`Metadata` instance, or ``None``. + input: A file path string, a ``vtkDataObject``, or ``None``. Used to infer a default name for the metadata + when *metadata* is None. + Returns: + The resolved :class:`Metadata` instance. + """ + if metadata is None: + # If there is no metadata provided, set the asset name to the file name (without extension) + # if input is not a string, default to "model" to avoid issues with unnamed datasets. + name = Path(input).stem if isinstance(input, str) else "model" + return Metadata(name=name, unit="") + + if isinstance(metadata, Metadata): + return metadata + + if not isinstance(metadata, str): + raise TypeError(f"Unsupported metadata type: {type(metadata).__name__}") + + return self.read_metadata(metadata) + + # ------------------------------------------------------------------ + # Save / load state + # ------------------------------------------------------------------ + + def get_state_file_path(self, state_dir: str) -> Path: + """Return the canonical path to ``visor.json`` inside *state_dir*. + + Args: + state_dir: Directory that contains (or will contain) the state file. + + Returns: + :class:`pathlib.Path` pointing at ``<state_dir>/visor.json``. + """ + return Path(state_dir) / _STATE_FILE_NAME + + def get_persisted_dataset_path(self, state_dir: str, dataset_name: str) -> Path: + """Return the canonical path to a persisted dataset snapshot inside *state_dir*. + + Args: + state_dir: Directory that contains (or will contain) the persisted dataset snapshot. + dataset_name: Name of the dataset (used to generate a unique file name). + + Returns: + :class:`pathlib.Path` pointing at the expected location of the persisted dataset snapshot. + """ + filename = f"{dataset_name}{_DATASET_SNAPSHOT_NAME_SUFFIX}" + return (Path(state_dir) / filename).with_suffix(f".{_DATASET_SNAPSHOT_EXTENSION}") + + def write_dataset(self, path: str | Path, dataset: VisorDatasetType) -> Path: + """Write *dataset* to disk as a VTKHDF file at *path*. + + Delegates to :func:`~ansys.visor.viewer.vtk.io.dataset_to_file.dataset_to_file`. + + Args: + path: Destination file path (should end with ``.vtkhdf``). + dataset: The VTK data object to serialise. + + Returns: + The :class:`pathlib.Path` of the file that was written. + """ + dest = dataset_to_file(path, dataset) + logger.info(f"Dataset snapshot written to {dest}") + return dest + + def build_metadata_for_load_state( + self, + dataset_name: str, + unit: str | None, + dataset_state: "PersistedDatasetState", + ) -> ExtendedMetadata: + """Build an :class:`ExtendedMetadata` for a dataset being restored from a save-state directory. + + The returned object carries: + + * ``name`` / ``unit`` โ€” taken from the persisted scene (stable across sessions). + * ``file_path`` โ€” set to ``source_file_path`` when available, so the registry + records where the data originally came from. + * ``metadata_path`` โ€” set to ``source_metadata_path`` when available. + * ``state`` โ€” the full :class:`PersistedDatasetState` (parts + paths) so that + part visibility and the serialized snapshot path are preserved. + + Args: + dataset_name: The stable dataset name stored in ``visor.json``. + unit: The scene unit stored in ``visor.json`` (may be ``None``). + dataset_state: The :class:`PersistedDatasetState` for this dataset. + + Returns: + A fully populated :class:`ExtendedMetadata` ready to pass to + :meth:`~ansys.visor.viewer.vtk.scene.base.VisorSceneBase.add_dataset`. + """ + return ExtendedMetadata( + name=dataset_name, + unit=unit or "", + state=dataset_state, + file_path=dataset_state.source_file_path, + metadata_path=dataset_state.source_metadata_path, + ) + + def delete_stale_snapshots(self, state_dir: str, state: PersistedViewerStateV1) -> list[Path]: + """Delete VTKHDF snapshot files in *state_dir* that are no longer referenced by *state*. + + Any ``*.vtkhdf`` file found in *state_dir* whose path does not appear in + ``state.scene.dataset_states`` is removed. This prevents snapshots from + previous saves accumulating on disk after a dataset has been removed. + + Args: + state_dir: Directory that was just written by :meth:`write_state`. + state: The state that was just persisted โ€” only snapshots referenced + here are kept. + + Returns: + A list of :class:`pathlib.Path` objects for the files that were deleted. + """ + kept_paths = { + Path(ds.serialized_dataset_path) + for ds in state.scene.dataset_states.values() + if ds.serialized_dataset_path + } + deleted: list[Path] = [] + for candidate in Path(state_dir).glob(f"*.{_DATASET_SNAPSHOT_EXTENSION}"): + if candidate not in kept_paths: + if _try_unlink(candidate): + deleted.append(candidate) + logger.info(f"Deleted stale snapshot: {candidate}") + return deleted + + def write_state(self, state_dir: str, state: PersistedViewerStateV1) -> Path: + """Serialise *state* to ``visor.json`` inside *state_dir*. + + The directory is created if it does not already exist. + + Args: + state_dir: Target directory. Created (including parents) if absent. + state: The persisted viewer state to write. + + Returns: + The :class:`pathlib.Path` of the file that was written. + """ + state_file = self.get_state_file_path(state_dir) + Path(state_dir).mkdir(parents=True, exist_ok=True) + + json_string = state.model_dump_json(indent=4, by_alias=True) + with open(state_file, "w") as fh: + fh.write(json_string) + + logger.info(f"State saved to {state_file}") + return state_file + + def read_state(self, state_dir: str) -> PersistedViewerStateV1: + """Read and parse ``visor.json`` from *state_dir*. + + Args: + state_dir: Directory that contains ``visor.json``. + + Returns: + The deserialised :class:`PersistedViewerStateV1`. + + Raises: + FileNotFoundError: If ``visor.json`` does not exist in *state_dir*. + """ + state_file = self.get_state_file_path(state_dir) + if not state_file.exists(): + msg = f"State file not found: {state_file}" + logger.error(msg) + raise FileNotFoundError(msg) + + with open(state_file, "r") as fh: + json_string = fh.read() + + state = PersistedViewerStateV1.model_validate_json(json_string) + logger.info(f"State loaded from {state_file}") + return state diff --git a/src/ansys/visor/viewer/vtk/node_pipeline.py b/src/ansys/visor/viewer/vtk/node_pipeline.py new file mode 100644 index 00000000..7adeb5ad --- /dev/null +++ b/src/ansys/visor/viewer/vtk/node_pipeline.py @@ -0,0 +1,127 @@ +""" +VtkNodePipeline +=============== + +A self-contained VTK rendering pipeline for a single scene-graph leaf node. + +Owns the three VTK objects that turn a raw dataset into something the +renderer can display: + +* ``base_algorithm`` -- geometry extraction (``vtkGeometryFilter`` for + unstructured grids; ``vtkAppendPolyData`` for poly data). +* ``mapper`` -- ``vtkPolyDataMapper`` connected to the algorithm output. +* ``actor`` -- ``vtkActor`` connected to the mapper. + +Created when a leaf node is registered with the renderer and destroyed when +that node is deregistered. The scene-graph node itself owns only the raw +dataset and metadata; no VTK pipeline objects live on it. +""" + +from dataclasses import dataclass +from typing import Callable, Optional + +from vtkmodules.vtkCommonDataModel import vtkDataObject, vtkPlane, vtkPolyData, vtkUnstructuredGrid +from vtkmodules.vtkCommonExecutionModel import vtkPolyDataAlgorithm +from vtkmodules.vtkFiltersCore import vtkAppendPolyData +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter +from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper + +from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +logger = VisorDefaultLogger(__name__) + + +@dataclass +class VtkNodePipeline: + """The VTK rendering pipeline for one scene-graph leaf (part) node. + + Attributes + ---------- + actor: + The ``vtkActor`` added to the renderer. + mapper: + The ``vtkPolyDataMapper`` driving the actor. + base_algorithm: + The geometry-extraction algorithm whose output feeds the mapper. + """ + + actor: vtkActor + mapper: vtkPolyDataMapper + base_algorithm: vtkPolyDataAlgorithm + + # ------------------------------------------------------------------ + # Factories + # ------------------------------------------------------------------ + + @classmethod + def from_dataset(cls, dataset: vtkDataObject) -> "VtkNodePipeline": + """Build a complete pipeline from ``dataset``. + + Parameters + ---------- + dataset: + A ``vtkUnstructuredGrid`` or ``vtkPolyData`` -- the raw dataset + owned by the scene-graph leaf node. + """ + base_algorithm = cls._create_base_algorithm(dataset) + + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(base_algorithm.GetOutputPort()) + + actor = vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetDiffuseColor(VisorColors.DefaultMeshColor) + actor.GetProperty().SetInterpolation(0) # flat shading + + return cls(actor=actor, mapper=mapper, base_algorithm=base_algorithm) + + + # ------------------------------------------------------------------ + # Mutation helpers + # ------------------------------------------------------------------ + + def set_clipping_plane(self, plane: Optional[vtkPlane]) -> None: + """Replace the mapper's clipping plane with ``plane`` (clear if ``None``).""" + self.mapper.RemoveAllClippingPlanes() + if plane is not None: + self.mapper.AddClippingPlane(plane) + + def update_input( + self, + algorithm_filter: Optional[Callable[[vtkPolyDataAlgorithm], vtkPolyDataAlgorithm]] = None, + ) -> None: + """Reconnect the mapper to a new algorithm output. + + Parameters + ---------- + algorithm_filter: + Optional callable that wraps ``base_algorithm`` in an additional + filter and returns the new downstream algorithm. When ``None``, + the mapper is reconnected directly to ``base_algorithm``. + """ + algorithm = self.base_algorithm + if algorithm_filter is not None: + algorithm = algorithm_filter(algorithm) + self.mapper.SetInputConnection(algorithm.GetOutputPort()) + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + + @staticmethod + def _create_base_algorithm(dataset: vtkDataObject) -> vtkPolyDataAlgorithm: + if isinstance(dataset, vtkUnstructuredGrid): + f = vtkGeometryFilter() + f.SetNonlinearSubdivisionLevel(0) + f.SetInputData(dataset) + return f + elif isinstance(dataset, vtkPolyData): + f = vtkAppendPolyData() + f.SetInputData(dataset) + return f + else: + msg = f"node type not yet supported: {type(dataset)}" + logger.error(msg) + raise RuntimeError(msg) + diff --git a/src/ansys/visor/viewer/vtk/scene/__init__.py b/src/ansys/visor/viewer/vtk/scene/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py new file mode 100644 index 00000000..efcadb0a --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -0,0 +1,355 @@ +"""VTK scene management for Visor Viewer.""" + +import json +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, List + +from trame_server import Server + +from ansys.visor.viewer.core.metadata import ExtendedMetadata +from ansys.visor.viewer.core.perf_timer import PerfTimer +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 +from ansys.visor.viewer.models.runtime.visor_scene_details import VisorSceneDetails +from ansys.visor.viewer.renderer.base import IRenderer +from ansys.visor.viewer.vtk.datasets.visor_dataset import VisorDataset +from ansys.visor.viewer.vtk.datasets.visor_dataset_registry import VisorDatasetRegistry +from ansys.visor.viewer.vtk.scene.scene_graph_state_builder import SceneGraphStateBuilder +from ansys.visor.viewer.vtk.scene.visor_state_mapper import VisorStateMapper +from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraph +from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables +from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate + +if TYPE_CHECKING: + from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState + +logger = VisorDefaultLogger(__name__) + +class VisorSceneBase(ABC): + """ + Abstract coordinator for a Visor viewer scene. + + Owns the **scene description** side of the application: + + * :class:`VisorVTKSceneGraph` โ€“ the pure scene-graph node tree + * :class:`VisorDatasetRegistry` โ€“ dataset metadata, variables, unit + * :class:`VisorStateMapper` โ€“ persisted โ†” runtime state conversion + + All **rendering** concerns are delegated to an injected :class:`IRenderer` + implementation (``self._vtk_renderer``): VTK pipeline objects, actor + lifecycle, camera, and scene-state serialisation. + + Subclasses must implement two abstract hooks that capture the difference + in *state authority* between rendering backends: + + * :meth:`_get_runtime_state_async` โ€” wasm path does a frontend round-trip; + RCA/headless paths build state server-side. + * :meth:`_apply_runtime_state_to_render` โ€” wasm path calls a JS + ``set_state``; RCA path pushes camera onto ``vtkCamera``; headless + is a no-op. + + This separation means that adding a new rendering backend requires only: + + 1. A new :class:`IRenderer` implementation. + 2. A new :class:`VisorSceneBase` subclass that overrides the two hooks. + """ + + _server: Server + _scene_graph: VisorSceneGraph | None + _dataset_registry: VisorDatasetRegistry + _renderer: IRenderer + _state_mapper: VisorStateMapper + + def __init__( + self, + server: Server, + dark_mode: bool = False, + renderer: IRenderer | None = None + ): + """Initialize the scene coordinator and its local renderer backend.""" + logger.debug("Initializing %s", type(self).__name__) + self.dark_mode: bool = dark_mode + self._server = server + self._scene_graph = None + self._pipelines = {} + + self._dataset_registry = self._initialize_dataset_registry() + + if renderer is not None: + self._renderer = renderer + else: + # Subclass must have assigned self._vtk_renderer before calling + # super().__init__() without a renderer, or override _initialize. + raise TypeError( + f"{type(self).__name__} must supply a renderer to VisorSceneBase.__init__" + ) + + self._initialize_scene_graph() + self._state_mapper = VisorStateMapper(self._dataset_registry) + + # ========================================================================= + # Abstract hooks โ€” subclasses differ on state authority + # ========================================================================= + + @abstractmethod + async def _get_runtime_state_async(self, timeout: float) -> "RuntimeAppState": + """ + Obtain the current runtime app state. + + * Wasm path: round-trip to the React frontend via + :class:`VisorFrontendBridge`. + * RCA / headless paths: build entirely from server-side VTK objects and + the dataset registry โ€” no frontend call. + """ + + @abstractmethod + def _apply_runtime_state_to_render(self, runtime_app_state: "RuntimeAppState") -> None: + """ + Push a runtime app state onto the renderer / frontend after the + shared per-part state has already been restored. + + * Wasm path: ``render_window_only()`` + JS ``set_state`` call. + * RCA path: push camera onto ``vtkCamera`` + ``render_window_only()``. + * Headless path: no-op. + """ + + # ========================================================================= + # Public properties + # ========================================================================= + + @property + def dataset_count(self) -> int: + """Number of datasets registered in the scene.""" + return self._dataset_registry.count + + @property + def datasets(self) -> dict[int, VisorDataset]: + """List of datasets registered in the scene.""" + return self._dataset_registry.datasets + + def list_all_dataset_info(self) -> dict[int, dict]: + """Convenience: metadata snapshot for UI without parts.""" + return self._dataset_registry.list_info() + + async def get_state(self, timeout: float) -> PersistedViewerStateV1: + """ + Capture the current viewer state and return it as a + :class:`PersistedViewerStateV1`. + + The renderer-specific part of state capture is handled by + :meth:`_get_runtime_state_async`. + """ + runtime_state = await self._get_runtime_state_async(timeout) + persisted = self._state_mapper.runtime_to_persisted(runtime_state) + return persisted + + def apply_state(self, state: PersistedViewerStateV1): + """ + Apply a saved viewer state. + + Shared work (per-part state restoration) is done here; the + renderer-specific final step is delegated to + :meth:`_apply_runtime_state_to_render`. + """ + # Apply UI settings + self.dark_mode = state.ui.dark_theme + + # Transform the frontend PersistedViewerStateV1 -> RuntimeAppState + runtime_app_state = self._state_mapper.persisted_to_runtime(state) + + # Renderer-specific: flush VTK window and notify frontend (wasm), or + # push camera to vtkCamera (RCA), or no-op (headless). + self._apply_runtime_state_to_render(runtime_app_state) + + def get_scene_details(self) -> VisorSceneDetails: + """Return the VisorState.""" + if self._scene_graph is None: + self._initialize_scene_graph() + annotation = self._renderer.build_renderer_annotation() + scene_graph_state = self._build_scene_graph_state() + return VisorSceneDetails.from_components( + dark_mode=self.dark_mode, + unit=self._dataset_registry.unit, + dataset_states=self._dataset_registry.runtime_state_dict, + scene_graph_state=scene_graph_state, + renderer_annotation=annotation, + ) + + def get_scene_details_json(self) -> str: + """Return the VisorVtkPipelineState as JSON string.""" + return json.dumps(self.get_scene_details().model_dump(exclude_none=True, by_alias=True)) + + def handle_save_state_response(self, request_id: int, response: dict) -> None: + """Called by LocalApp trigger when the frontend responds.""" + self._frontend_bridge.resolve_save_state_response(request_id, response) + + def clear(self): + """Remove all actors from the renderer and reset the scene.""" + if self._scene_graph is not None: + self._renderer.deregister_all() + self._dataset_registry.clear() + self._scene_graph = None + + def populate_scene(self): + """Update widgets to reflect the current scene contents. + + Actor attach is done per-leaf inside :meth:`add_dataset` via + :meth:`IRenderer.register_node`, so this method only refreshes the + widget bounds and count. + """ + if self._scene_graph is None: + self._initialize_scene_graph() + self.update_widgets() + + def finalize_scene(self, skip_reset_camera: bool = False): + """Populate the scene, reset the camera if applicable, then render.""" + self.populate_scene() + + if not skip_reset_camera: + # if there is one or zero datasets in the scene, reset the camera + if self._dataset_registry.count <= 1: + self.reset_camera() + + self.render() + + def update_widgets(self): + """Reset the widgets to fit the scene graph bounds.""" + if self._scene_graph is None: + msg = "Scene graph has not been initialized, cannot reset widgets and camera." + logger.error(msg) + raise RuntimeError(msg) + + self._update_widget_bounds() + self._update_actor_count() + + def add_dataset(self, input: VisorDatasetType, metadata: ExtendedMetadata) -> int: + """Set the input dataset and metadata for the scene graph.""" + if input is None: + msg = "Input dataset is None, cannot load dataset." + logger.error(msg) + raise ValueError(msg) + + # Ensure unique dataset name + dataset_name = self._dataset_registry.get_sanitized_metadata_name(metadata) + + # Initialize the scene graph if it does not exist + if self._scene_graph is None: + self._initialize_scene_graph() + + # Load the dataset into the scene graph + dataset_id = self._scene_graph.load_dataset(input, dataset_name) + + # Register each leaf's pipeline with the renderer. + subtree = self._scene_graph.get_descendant_node(dataset_id, include_self=True) + if subtree is not None: + for leaf in subtree.get_descendant_part_nodes(include_self=True): + self._renderer.register_node(leaf, leaf.dataset) + + # Seed PartIndex with scene-graph node IDs so that part_id == scene-graph node ID, + # which is the contract the frontend relies on to apply per-part state (opacity etc.). + part_name_to_id = self._scene_graph.get_part_name_to_id_map(dataset_id) + + self._dataset_registry.add(dataset_id, dataset_name, input, part_name_to_id, metadata) + + return dataset_id + + def remove_dataset(self, dataset_id: int): + """Remove a dataset from the scene graph.""" + if self._scene_graph is None: + msg = "Scene graph has not been initialized, cannot load dataset." + logger.error(msg) + raise RuntimeError(msg) + + # Deregister each leaf's pipeline from the renderer before dropping + # the subtree from the scene graph. + node = self._scene_graph.get_descendant_node(dataset_id) + if node is None: + msg = f"Dataset with id {dataset_id} not found in scene graph." + logger.error(msg) + raise ValueError(msg) + + for leaf in node.get_descendant_part_nodes(include_self=True): + self._renderer.deregister_node(leaf.id) + + # Remove dataset from the scene graph + self._scene_graph.remove_dataset(dataset_id) + + # Unregister the dataset + self._dataset_registry.remove(dataset_id) + + def list_variables_for_dataset(self, dataset_id: int) -> List[VisorPartVariables]: + return self._dataset_registry.list_variables(dataset_id) + + def update_variables_for_dataset(self, dataset_id: int, variables: List[VisorVariableUpdate]) -> None: + """Update the variables for the dataset in the scene graph.""" + timer = PerfTimer("update_variables_for_dataset", logger, dataset=dataset_id) + + with timer.phase("dataset_registry.update"): + self._dataset_registry.update_variables(dataset_id, variables) + + # Refresh cached variable metadata on part nodes without re-wiring + # the VTK pipeline (which would call SetInputConnection + mark the + # mapper Modified, causing unnecessary re-serialisation of geometry + # that has not changed). + dataset_node = self._scene_graph.get_descendant_node(dataset_id) + with timer.phase("update_descendant_parts"): + dataset_node.refresh_descendant_variable_metadata(include_self=True) + + with timer.phase("render"): + self.render() + + timer.log() + + def render(self): + """Delegate to the renderer backend.""" + self._renderer.render() + + def reset_camera(self): + """Reset the camera to fit the scene graph bounds.""" + if self._scene_graph is None: + logger.debug("Scene graph not initialized; skipping camera reset.") + return + + self._renderer.reset_camera(self._scene_graph.bounds) + + def pick_geometry(self, actor_wasm_id, cell_id, mode, world_x, world_y, world_z) -> dict: + """ + Frontend-trigger entry point for cell picking. Packs the world-space + pick position into a tuple and delegates to the renderer backend. + """ + return self._renderer.pick_geometry( + actor_wasm_id, cell_id, mode, (world_x, world_y, world_z) + ) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _initialize_scene_graph(self): + """Initialize an empty scene graph.""" + self._scene_graph = VisorSceneGraph() + + def _initialize_dataset_registry(self) -> VisorDatasetRegistry: + """Initialize the dataset registry to manage datasets in the scene.""" + return VisorDatasetRegistry() + + def _update_widget_bounds(self): + """Update the cross-section and bounding box widgets with the current scene graph bounds.""" + self._renderer.update_bounds(self._scene_graph.bounds) + + def _update_actor_count(self): + """Update the bounding box widget with the current part-node count.""" + self._renderer.update_actor_count(self._scene_graph.descendant_part_count()) + + def _build_scene_graph_state(self): + """Assemble the pure scene-graph ``SceneGraphNodeInfo`` tree. + + Structure and metadata only, read from the scene-graph nodes. Renderer + handles live entirely on the renderer's annotation + (:meth:`IRenderer.build_renderer_annotation`); this method never reads + the renderer. + """ + return SceneGraphStateBuilder(self._scene_graph).build() + + diff --git a/src/ansys/visor/viewer/vtk/scene/local_scene.py b/src/ansys/visor/viewer/vtk/scene/local_scene.py new file mode 100644 index 00000000..854c67b0 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene/local_scene.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING + +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.vtk.scene.base import VisorSceneBase + +if TYPE_CHECKING: + from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState + +logger = VisorDefaultLogger(__name__) + +from trame_server import Server + +# ============================================================================= +# VisorScene โ€” wasm/LocalView subclass (the original production scene) +# ============================================================================= + +class VisorLocalScene(VisorSceneBase): + """ + Coordinator for a Visor viewer scene using the wasm/LocalView rendering path. + + The React frontend is authority for camera state. :meth:`get_state` asks + the frontend for the current camera via :class:`VisorFrontendBridge` and + waits for the response. :meth:`apply_state` pushes the restored state back + to the frontend via a JS ``set_state`` call. + + This is the scene used by + :class:`~ansys.visor.viewer.app.visor_vtk_local.VisorVTKLocal` + (``RenderingMode.LOCAL``). + """ + + def __init__(self, server: Server, dark_mode: bool = False): + from ansys.visor.viewer.renderer.local_renderer import VisorLocalRenderer + from ansys.visor.viewer.vtk.scene.visor_frontend_bridge import VisorFrontendBridge + + try: + vtk_renderer = VisorLocalRenderer(server) + super().__init__(server, dark_mode, renderer=vtk_renderer) + self._frontend_bridge = VisorFrontendBridge(server, self._renderer.frontend_ref_name) + except RuntimeError as e: + msg = f"Failed to initialize VTK pipeline: {e}" + logger.error(msg) + raise RuntimeError(msg) from e + + # ------------------------------------------------------------------------- + # State authority hooks โ€” wasm: frontend round-trip + # ------------------------------------------------------------------------- + + async def _get_runtime_state_async(self, timeout: float) -> "RuntimeAppState": + """Ask the React frontend for the current app state (camera, UI, โ€ฆ).""" + response = await self._frontend_bridge.request_state(timeout=timeout) + return response.app_state + + def _apply_runtime_state_to_render(self, runtime_app_state: "RuntimeAppState") -> None: + """ + Flush the VTK window then push the restored state to the React frontend. + + Use render_window_only() โ€” NOT the full render() which also calls + local_view.update(). local_view.update() fires onServerUpdateAsync on + the React frontend, which clears the setState event listener while it + rebuilds the scene. If the set_state() JS call below arrives during + that window the persisted state is silently lost. + finalize_scene() (called just before apply_state when loading from an + empty scene) has already done a full render + wasm sync; all we need + here is a lightweight VTK flush before the JS payload is sent. + """ + self._renderer.render_window_only() + self._frontend_bridge.set_state(runtime_app_state) + + # ------------------------------------------------------------------------- + # Wasm-specific helpers (not part of VisorSceneBase) + # ------------------------------------------------------------------------- + + def handle_save_state_response(self, request_id: int, response: dict) -> None: + """Called by LocalApp trigger when the frontend responds.""" + self._frontend_bridge.resolve_save_state_response(request_id, response) + + def cleanup_state(self) -> None: + """Remove transient wasm keys from trame server state.""" + self._server.state.pop("wasm_ids", None) + self._server.state.pop("wasm_ref_name", None) + self._server.state.pop("perf_logging", None) + diff --git a/src/ansys/visor/viewer/vtk/scene/scene_graph_state_builder.py b/src/ansys/visor/viewer/vtk/scene/scene_graph_state_builder.py new file mode 100644 index 00000000..de8f56fa --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene/scene_graph_state_builder.py @@ -0,0 +1,33 @@ +"""Wire-format builder for scene-graph state.""" + +from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo +from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphNode + + +class SceneGraphStateBuilder: + """Compose a :class:`SceneGraphNodeInfo` tree from the scene graph. + + Reads structure and metadata only, from the scene-graph nodes. Carries + no renderer handles; those are stamped separately by the caller from the + renderer's annotation. + """ + + def __init__(self, scene_graph: VisorSceneGraphNode): + self._scene_graph = scene_graph + + def build(self) -> SceneGraphNodeInfo: + """Return the assembled ``SceneGraphNodeInfo`` for the whole graph.""" + return self._build_node(self._scene_graph) + + # ------------------------------------------------------------------ + # Internal + # ------------------------------------------------------------------ + + def _build_node(self, node: VisorSceneGraphNode) -> SceneGraphNodeInfo: + info = node.state + if node.is_group_node: + # Recurse so leaves further down the tree are reachable too. + info.children = [self._build_node(c) for c in node.children] + return info + + diff --git a/src/ansys/visor/viewer/vtk/scene/visor_frontend_bridge.py b/src/ansys/visor/viewer/vtk/scene/visor_frontend_bridge.py new file mode 100644 index 00000000..0df6d672 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene/visor_frontend_bridge.py @@ -0,0 +1,149 @@ +"""Frontend bridge. + +This module contains :class:`FrontendBridge`, a small abstraction around making calls +into the frontend (via ``server.js_call``) and correlating async responses. + +Important: This class intentionally mirrors the previous logic that lived in +``base.py``. It aims for parity, not enhancements. +""" + +from __future__ import annotations + +import asyncio +import threading +from typing import Dict, Optional + +from pydantic import ValidationError + +from ansys.visor.viewer.core.visor_helpers import get_random_javascript_safe_id +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.models.runtime.requests.visor_load_state_request import VisorLoadStateRequest +from ansys.visor.viewer.models.runtime.requests.visor_save_state_request import VisorSaveStateRequest +from ansys.visor.viewer.models.runtime.requests.visor_save_state_response import VisorSaveStateResponse + +logger = VisorDefaultLogger(__name__) + + +class VisorFrontendBridge: + """Bridge for JS -> Python request/response flows. + + Currently used for the "getState" request, where the frontend later triggers + back a payload that we validate and resolve against the pending async waiter. + """ + + def __init__(self, server, ref_name: str): + self._server = server + self._ref_name = ref_name + + self._pending_lock = threading.Lock() + # request_id (int) -> asyncio.Future[VisorSaveStateResponse] + self._pending_requests: Dict[int, asyncio.Future] = {} + # The asyncio loop that owns the pending futures (set on first request call) + self._pending_loop: Optional[asyncio.AbstractEventLoop] = None + + def set_state(self, runtime_app_state) -> int: + """Send a load-state request to the frontend. + + This method intentionally mirrors the exact logic that previously lived in + ``VisorScene.apply_state`` (generate request_id, create VisorLoadStateRequest, + js_call "setState" with model_dump). + + Returns + ------- + int + The generated request_id. + """ + request_id = get_random_javascript_safe_id() + load_request = VisorLoadStateRequest(request_id=request_id, app_state=runtime_app_state) + self._server.js_call(self._ref_name, "setState", load_request.model_dump(by_alias=True)) + return int(request_id) + + async def request_state(self, timeout: float) -> VisorSaveStateResponse: + """Request frontend state and await response. + + Mirrors the previous ``VisorScene.get_state`` request/await logic. + """ + request_id = get_random_javascript_safe_id() + + loop = asyncio.get_running_loop() + fut: asyncio.Future = loop.create_future() + with self._pending_lock: + # Remember which loop owns these futures so callbacks from other threads can complete them safely. + self._pending_loop = loop + self._pending_requests[int(request_id)] = fut + + save_request = VisorSaveStateRequest(request_id=request_id) + self._server.js_call(self._ref_name, "getState", save_request.model_dump(exclude_none=True, by_alias=True)) + + try: + response = await asyncio.wait_for(fut, timeout=timeout) + finally: + with self._pending_lock: + self._pending_requests.pop(int(request_id), None) + # If no pending requests remain, clear loop reference to avoid keeping it alive. + if not self._pending_requests: + self._pending_loop = None + + return response + + @staticmethod + def _threadsafe_set_exception( + loop: Optional[asyncio.AbstractEventLoop], + f: asyncio.Future, exc: BaseException + ) -> None: + if f is None or f.done(): + return + if loop is not None: + loop.call_soon_threadsafe(f.set_exception, exc) + else: + # Best-effort fallback (should only happen if no async waiter exists) + f.set_exception(exc) + + @staticmethod + def _threadsafe_set_result( + loop: Optional[asyncio.AbstractEventLoop], + f: asyncio.Future, + value: VisorSaveStateResponse, + ) -> None: + if f is None or f.done(): + return + if loop is not None: + loop.call_soon_threadsafe(f.set_result, value) + else: + # Best-effort fallback (should only happen if no async waiter exists) + f.set_result(value) + + def resolve_save_state_response(self, request_id: int, response: dict) -> None: + """Resolve a pending request from a frontend callback. + + This is meant to be called by the Trame trigger when the frontend replies. + """ + # Snapshot loop for threadsafe scheduling (callback may run off the loop thread) + with self._pending_lock: + loop = self._pending_loop + + try: + save_state_response = VisorSaveStateResponse.model_validate(response) + except ValidationError as e: + print(f"Invalid save state response received: {e}") + # Fail fast: if we can't parse the response, any awaiting request call + # would otherwise hang until timeout. Propagate the error to pending waiters. + with self._pending_lock: + fut = self._pending_requests.get(request_id) + self._threadsafe_set_exception(loop, fut, e) + raise ValueError(f"Invalid save state response: {e}") from e + except Exception as e: + print("Unexpected error while handling save state response") + with self._pending_lock: + fut = self._pending_requests.get(request_id) + self._threadsafe_set_exception(loop, fut, e) + raise + + + with self._pending_lock: + fut = self._pending_requests.get(request_id) + + if fut: + self._threadsafe_set_result(loop, fut, save_state_response) + else: + logger.warning(f"Late save_state_response with id={request_id} (no pending async waiter)") diff --git a/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py b/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py new file mode 100644 index 00000000..6df9034b --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py @@ -0,0 +1,116 @@ +"""State mapping helpers. + +This module defines :class:`VisorStateMapper`, which is responsible for converting +between: + +- frontend/runtime state (:class:`~ansys.visor.viewer.models.runtime.scene.runtime_app_state.RuntimeAppState`) +- persisted viewer state (:class:`~ansys.visor.viewer.models.persist.persisted_viewer_state.PersistedViewerStateV1`) + +The dataset registry is required to look up :class:`VisorDataset` objects by ID or +name. Per-dataset translation between runtime part IDs and persisted part names is +delegated to :meth:`VisorDataset.runtime_to_persisted_state` and +:meth:`VisorDataset.persisted_to_runtime_state`. +""" + +from __future__ import annotations + +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 +from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState +from ansys.visor.viewer.vtk.datasets.visor_dataset_registry import VisorDatasetRegistry + +logger = VisorDefaultLogger(__name__) + + +class VisorStateMapper: + """Convert between runtime and persisted viewer state.""" + + def __init__(self, dataset_registry: VisorDatasetRegistry): + """Initialize the mapper.""" + if dataset_registry is None: + raise ValueError("dataset_registry is required") + self._dataset_registry = dataset_registry + + def runtime_to_persisted(self, runtime_app_state: RuntimeAppState) -> PersistedViewerStateV1: + """Convert frontend RuntimeAppState to PersistedViewerStateV1. + + Logic parity with ``VisorScene._runtime_to_persisted``. + """ + # UI + ui_state = runtime_app_state.ui + + # Scene + scene_state = runtime_app_state.scene + # Scene - unit + unit = scene_state.unit + # Scene - camera + camera = scene_state.camera + # Scene - variables: Pass through as-is since they are already keyed by stable variable identifier + variable_states = scene_state.spectrum_states + # Scene - datasets + runtime_dataset_states = scene_state.dataset_states + persisted_dataset_states = {} + for dataset_id, dataset_state in runtime_dataset_states.items(): + dataset = self._dataset_registry.datasets.get(dataset_id) + if dataset is None: + continue + persisted_dataset_states[dataset.name] = dataset.runtime_to_persisted_state(dataset_state) + + return PersistedViewerStateV1.from_components( + ui_state=ui_state, + camera=camera, + unit=unit, + cross_section=scene_state.cross_section, + orthographic_enabled=scene_state.orthographic_enabled, + cross_section_enabled=scene_state.cross_section_enabled, + edges_enabled=scene_state.edges_enabled, + bounding_box_enabled=scene_state.bounding_box_enabled, + datasets=persisted_dataset_states, + variable_states=variable_states, + ) + + def persisted_to_runtime(self, state: PersistedViewerStateV1) -> RuntimeAppState: + """Convert PersistedViewerStateV1 back to runtime RuntimeAppState. + + Logic parity with ``VisorScene._persisted_to_runtime``. + """ + # UI settings + ui_state = state.ui + + # scene state + scene_state = state.scene + + # scene - unit + unit = scene_state.unit + + # scene - camera + camera = scene_state.camera + + # scene - variables: Pass through as-is since they are already keyed by stable variable identifier + variable_states = scene_state.variable_states or {} + + # scene - datasets: Convert persisted dataset states to runtime + runtime_dataset_states = {} + for dataset_name, persisted_dataset_state in scene_state.dataset_states.items(): + # dataset_key is the dataset name + + # Get the VisorDataset object by name from the registry + dataset = self._dataset_registry.get_by_name(dataset_name) + if dataset is None: + logger.warning(f"Dataset with name '{dataset_name}' not found in registry. Skipping.") + continue + runtime_state = dataset.persisted_to_runtime_state(persisted_dataset_state.parts) + runtime_dataset_states[dataset.id] = runtime_state + + return RuntimeAppState.from_components( + dark_mode=ui_state.dark_theme, + unit=unit, + camera=camera, + cross_section=scene_state.cross_section, + orthographic_enabled=scene_state.orthographic_enabled, + cross_section_enabled=scene_state.cross_section_enabled, + edges_enabled=scene_state.edges_enabled, + bounding_box_enabled=scene_state.bounding_box_enabled, + dataset_states=runtime_dataset_states, + variable_states=variable_states, + ) diff --git a/src/ansys/visor/viewer/vtk/scene_graph/__init__.py b/src/ansys/visor/viewer/vtk/scene_graph/__init__.py new file mode 100644 index 00000000..aba561bb --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene_graph/__init__.py @@ -0,0 +1,8 @@ +from ansys.visor.viewer.vtk.scene_graph.base_node import VisorSceneGraphNode +from ansys.visor.viewer.vtk.scene_graph.group_node import VisorSceneGraphGroupNode +from ansys.visor.viewer.vtk.scene_graph.part_node import VisorSceneGraphPartNode +from ansys.visor.viewer.vtk.scene_graph.root_node import VisorSceneGraph + +__all__ = ['VisorSceneGraphNode', 'VisorSceneGraphGroupNode', + 'VisorSceneGraph', 'VisorSceneGraphPartNode'] + diff --git a/src/ansys/visor/viewer/vtk/scene_graph/base_node.py b/src/ansys/visor/viewer/vtk/scene_graph/base_node.py new file mode 100644 index 00000000..25426fc6 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene_graph/base_node.py @@ -0,0 +1,370 @@ +"""Class to represent node in Visor's scene graph""" + +import json +from abc import ABC, abstractmethod +from typing import ( + Callable, + Optional, + Union, +) + +from typing_extensions import Self +from vtkmodules.vtkCommonCore import ( + vtkInformation, + vtkInformationStringVectorKey, +) +from vtkmodules.vtkCommonDataModel import ( + vtkCompositeDataSet, +) + +from ansys.visor.viewer.core.visor_enums import VisorNodeType +from ansys.visor.viewer.core.visor_helpers import get_random_javascript_safe_id, is_composite_dataset +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo + +logger = VisorDefaultLogger(__name__) + +class NodeCache: + """ + A simple cache for storing descendant nodes to avoid recomputation. + """ + def __init__(self): + """Initialize a cache for descendant nodes.""" + self._cache = {} + + def make_key(self, kind, include_self, filter_func): + """Create a cache key for descendant nodes.""" + filter_key = self._filter_func_cache_key(filter_func) + return (kind, include_self, filter_key) + + def get(self, key): + """Return a descendant node for the given key.""" + return self._cache.get(key) + + def set(self, key, value): + """Set a descendant node for the given key.""" + self._cache[key] = value + + def clear(self): + """Clear the cache.""" + self._cache.clear() + + @staticmethod + def _filter_func_cache_key(filter_func): + """Function to generate a cache key for a filter function.""" + if filter_func is None: + return None + if hasattr(filter_func, "__code__"): + code = filter_func.__code__ + return code.co_names + if hasattr(filter_func, "__qualname__"): + return (filter_func.__module__, filter_func.__qualname__) + return repr(filter_func) + + +class VisorSceneGraphNode(ABC): + """ + Abstract base class representing a node in the Visor scene graph. + + Each node corresponds to either a dataset or a group of datasets in the scene graph. + This class defines the common interface and properties for all scene graph nodes, + including methods for initialization, serialization, and actor management. + + Subclasses must implement methods for post-initialization and updating descendant or self actors. + + Attributes: + METADATA_NAME_KEY: Key used to retrieve the node's name from VTK metadata. + _id: Unique identifier for the node. + _node_type: Type of the node (e.g., group or actor). + _name: name of the node. + _vtk_dataset_type: String representing the VTK dataset type. + _root_node: Reference to the root node of the scene graph. + _bounds: Bounding box of the node. + _cache: Cache for descendant nodes to avoid recomputation. + """ + METADATA_NAME_KEY: vtkInformationStringVectorKey = vtkCompositeDataSet.NAME() + + def __init__( + self, + # parent should be None if this is the root node + parent: Self | None, + # node_metadata should be None if this is the root node + node_metadata: vtkInformation | None = None, + # node should be a vtkDataObject or a subclass of it, + # or None if this is the root node + dataset: VisorDatasetType | None = None, + root_node: Self | None = None + ): + """Initialize a VisorSceneGraphNode instance.""" + self._id: int = get_random_javascript_safe_id() + self._node_type: VisorNodeType + self._name: str = "" + self._vtk_dataset_type: str + self._root_node: VisorSceneGraphNode = root_node + self._bounds: list[float] + self._cache = NodeCache() + + # Post-initialization method to set up the node. + self._post_init(parent, node_metadata, dataset, root_node) + + @abstractmethod + def _post_init( + self, + parent: Self | None, + metadata: vtkInformation | None = None, + dataset: VisorDatasetType | None = None, + root_node: Self | None = None + ): + """ + Abstract method for performing post-initialization logic specific to the node type. + + This method should be implemented by subclasses to handle additional setup after + the base class constructor has initialized common attributes. Typical tasks include + setting node-specific properties, processing the dataset, and updating collections. + + Args: + parent (Self | None): The parent node, or None if this is the root node. + node_metadata (vtkInformation | None): Optional VTK metadata for the node. + dataset (vtk_dataset_type | None): The VTK dataset associated with this node. + root_node (Self | None): Reference to the root node of the scene graph. + + Raises: + NotImplementedError: If not implemented in a subclass. + """ + raise NotImplementedError + + def get_descendant_nodes( + self, + include_self: bool = False, + filter_func: Optional[Callable[['VisorSceneGraphNode'], bool]] = None + ) -> list['VisorSceneGraphNode']: + """Return a list of descendant nodes.""" + nodes = [] + for node in self._get_descendant_node_dict(include_self, filter_func).values(): + nodes.append(node) + return nodes + + def get_descendant_node(self, node_id: int, include_self=False) -> Optional['VisorSceneGraphNode']: + """ Return a descendant node by its ID.""" + return self._get_descendant_node_dict(include_self=include_self).get(node_id, None) + + def get_descendant_part_node(self, node_id: int, include_self=False) -> Optional['VisorSceneGraphNode']: + """Return a descendant leaf (part) node by its ID.""" + return self._get_descendant_node_dict( + include_self=include_self, + filter_func=lambda node: node.is_part_node + ).get(node_id, None) + + def get_descendant_part_nodes(self, + include_self: bool = False + ) -> list['VisorSceneGraphNode']: + """Return descendant nodes that are leaf (part) nodes.""" + return self.get_descendant_nodes( + include_self=include_self, + filter_func=lambda node: node.is_part_node + ) + + def refresh_descendant_variable_metadata(self, include_self=False) -> None: + """ + Refresh the cached variable metadata on all descendant part nodes without + re-connecting the VTK pipeline (i.e. without calling SetInputConnection or + marking the mapper Modified). Use this after an in-place variable data update + where the pipeline topology has not changed. + """ + for node in self.get_descendant_part_nodes(include_self=include_self): + node.refresh_variable_metadata() + + def descendant_part_count(self, include_self=False) -> int: + """Return the number of descendant part nodes (optionally including self).""" + return len(self.get_descendant_part_nodes(include_self=include_self)) + + def get_descendant_node_name_to_id_map(self) -> dict[str, int]: + """ + Get a mapping of part names to their corresponding node IDs + for a specific dataset. + + Returns: + dict[str, int]: A dictionary mapping part names to node IDs. + """ + return {node.name: id for id, node in self._get_descendant_node_dict(include_self=True).items() if node.name is not None} + + @staticmethod + def get_node( + parent: 'VisorSceneGraphNode', + node_metadata: vtkInformation | None = None, + dataset: VisorDatasetType | None = None, + root_node: Union['VisorSceneGraphNode', None] = None + ) -> 'VisorSceneGraphNode': + """ + Create and return an appropriate VisorSceneGraphNode subclass instance based + on the provided VTK dataset type. + + This factory method inspects the type of the given `dataset` and instantiates + either a group node (`VisorSceneGraphGroupNode`) or a leaf part node + (`VisorSceneGraphPartNode`). If the dataset type is not supported, + a RuntimeError is raised. + + Args: + parent (VisorSceneGraphNode): The parent node in the scene graph. + node_metadata (vtkInformation | None): Optional VTK metadata for the node. + dataset (vtk_dataset_type | None): The VTK dataset associated with this node. + root_node (VisorSceneGraphNode | None): Ref to the root node of the scene graph. + + Returns: + VisorSceneGraphNode: An instance of the appropriate subclass for the dataset type. + + Raises: + RuntimeError: If the dataset type is not supported. + """ + from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphGroupNode, VisorSceneGraphPartNode + + subclass_map = {VisorNodeType.GROUP: VisorSceneGraphGroupNode, + VisorNodeType.PART: VisorSceneGraphPartNode} + try: + node_type = VisorSceneGraphNode.get_node_type_from_dataset(dataset) + subclass = subclass_map[node_type] + return subclass(parent, node_metadata, dataset, root_node) + except RuntimeError as e: + msg = f"could not create scene graph node: {e}" + logger.error(msg) + raise RuntimeError(msg) + + @staticmethod + def get_node_type_from_dataset(dataset) -> VisorNodeType | None: + """ Determine the node type based on the dataset type.""" + # Determine if the dataset is composite + # Raises RuntimeError if dataset is unsupported + is_composite = is_composite_dataset(dataset) + if is_composite: + return VisorNodeType.GROUP + else: + return VisorNodeType.PART + + def remove_node(self, node_id: int) -> bool: + """ + Remove a child node and its children from this node by its ID. + """ + if not self.is_group_node: + logger.warning("Cannot remove a node from an actor node.") + return False + + children = self._children + for i, child in enumerate(children): + if child.id == node_id: + # Remove the child and its descendants + child._cleanup_descendants() + del children[i] + self._clear_cache() + return True + if child.remove_node(node_id): + self._clear_cache() + return True + return False + + def _cleanup_descendants(self): + """Recursively clean up all descendants.""" + if not self.is_group_node: + return + for child in self._children: + child._cleanup_descendants() + self._children = [] + + def get_json(self) -> str: + """ + Return a cached JSON string representation of the node. + + Returns: + str: The JSON string representation of the node. + """ + return json.dumps(self.state.model_dump()) + + @property + def id(self): + """Return the node ID.""" + return self._id + + @property + def name(self): + """Return the node name.""" + return self._name + + @property + def vtk_dataset_type(self): + """Return the node VTK dataset type.""" + return self._vtk_dataset_type + + + @property + def is_part_node(self) -> bool: + """Return whether the node is a leaf (renderable) part node.""" + return self._node_type == VisorNodeType.PART + + @property + def is_group_node(self) -> bool: + """Return whether the node is a group node.""" + return self._node_type in [VisorNodeType.GROUP, VisorNodeType.ROOT] + + @property + def bounds(self) -> list[float]: + """Return the bounds of the node.""" + return self._bounds + + @property + def state(self) -> SceneGraphNodeInfo: + """Return the scene graph node info""" + return SceneGraphNodeInfo( + id=self.id, + name=self.name, + isActorNode=self.is_part_node, + isGroupNode=self.is_group_node, + nodeType=self._vtk_dataset_type, + bounds=self.bounds, + ) + + def _set_name(self, node_metadata: vtkInformation | None = None): + """ + Set the name of the node. + This method is called during initialization to set the name of the node. + If the name is not set in the metadata, it defaults to "untitled". + """ + if node_metadata is None: + msg = "node_metadata should not be null here" + logger.error(msg) + raise RuntimeError(msg) + elif node_metadata.Has(self.METADATA_NAME_KEY): + self._name = node_metadata.Get(self.METADATA_NAME_KEY) + if not self._name: + self._name = "untitled" + + def _get_descendant_node_dict( + self, + include_self: bool = False, + filter_func: Optional[Callable[['VisorSceneGraphNode'], bool]] = None + ) -> dict[int, 'VisorSceneGraphNode']: + """Return the descendant node dictionary.""" + cache_key = self._cache.make_key("node_dict", include_self, filter_func) + cached = self._cache.get(cache_key) + if cached is not None: + return cached + + node_dict = {} + if include_self: + if filter_func is None or filter_func(self): + node_dict[self.id] = self + for child in getattr(self, "_children", []): + child_dict = child._get_descendant_node_dict(include_self=True, filter_func=filter_func) + node_dict.update(child_dict) + + self._cache.set(cache_key, node_dict) + return node_dict + + def _clear_cache(self): + """ Clear the cache for this node and all its children.""" + self._cache.clear() + for child in getattr(self, "_children", []): + child._clear_cache() + + + + diff --git a/src/ansys/visor/viewer/vtk/scene_graph/group_node.py b/src/ansys/visor/viewer/vtk/scene_graph/group_node.py new file mode 100644 index 00000000..df324e08 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene_graph/group_node.py @@ -0,0 +1,94 @@ +"""Class for representing a group node in the scene graph.""" + +from typing_extensions import Self +from vtkmodules.vtkCommonCore import ( + vtkInformation, +) +from vtkmodules.vtkCommonDataModel import vtkDataObject, vtkMultiBlockDataSet, vtkMultiPieceDataSet + +from ansys.visor.viewer.core.visor_enums import VisorNodeType +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphNode + +logger = VisorDefaultLogger(__name__) + + +class VisorSceneGraphGroupNode(VisorSceneGraphNode): + """ + Represents a group node in the Visor scene graph. + + This node organizes and manages child nodes, which can be either group nodes or actor nodes. + Group nodes do not have associated VTK actors but serve as containers for hierarchical scene structure. + """ + def __init__(self, + # parent should be None if this is the root node + parent: Self | None, + # node_metadata should be None if this is the root node + node_metadata: vtkInformation | None = None, + # node should be a vtkDataObject or a subclass of it, or None if this is the root node + dataset: vtkDataObject | vtkMultiBlockDataSet | vtkMultiPieceDataSet | None = None, + root_node: Self | None = None + ): + self._children: list[VisorSceneGraphNode] = [] + super().__init__(parent, node_metadata, dataset, root_node) + + def add_child(self, child: VisorSceneGraphNode): + """ + Add a child node to this group node. + + Args: + child (VisorSceneGraphNode): The child node to add. + """ + self._children.append(child) + child.Parent = self + self._clear_cache() + + def _post_init(self, + parent: Self | None, + node_metadata: vtkInformation | None = None, + dataset: vtkDataObject | vtkMultiBlockDataSet | vtkMultiPieceDataSet | None = None, + root_node: Self | None = None + ): + """ + Initialize the group node with the given parameters. + """ + self._node_type = VisorNodeType.GROUP + self._set_name(node_metadata) + + num: int = 0 + if isinstance(dataset, vtkMultiBlockDataSet): + """""" + self._vtk_dataset_type = "vtkMultiBlockDataSet" + num = dataset.GetNumberOfBlocks() + elif isinstance(dataset, vtkMultiPieceDataSet): + """""" + self._vtk_dataset_type = "vtkMultiPieceDataSet" + num = dataset.GetNumberOfPieces() + else: + """""" + msg = f"node type not yet supported: {type(dataset)}" + logger.error(msg) + raise RuntimeError(msg) + + self._bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + dataset.GetBounds(self._bounds) + + for i in range(num): + child = dataset.GetBlock(i) + child_metadata: vtkInformation = dataset.GetMetaData(i) + item = VisorSceneGraphNode.get_node(self, child_metadata, child, root_node) + # register the item in the node's collections + self.add_child(item) + + @property + def children(self): + """Return the children of this node.""" + return self._children + + @property + def state(self): + """Return the state of this node.""" + state_obj = super().state + state_obj.children = [child.state for child in self.children] + return state_obj + diff --git a/src/ansys/visor/viewer/vtk/scene_graph/part_node.py b/src/ansys/visor/viewer/vtk/scene_graph/part_node.py new file mode 100644 index 00000000..f3c987cd --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene_graph/part_node.py @@ -0,0 +1,118 @@ +"""Scene-graph leaf node that owns a raw VTK dataset and scene metadata.""" + +from typing import List + +from typing_extensions import Self +from vtkmodules.util.data_model import PolyData, UnstructuredGrid +from vtkmodules.vtkCommonCore import vtkInformation +from vtkmodules.vtkCommonDataModel import vtkDataObject, vtkPolyData, vtkUnstructuredGrid + +from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.core.visor_enums import VisorNodeType +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo +from ansys.visor.viewer.vtk.scene_graph.base_node import VisorSceneGraphNode +from ansys.visor.viewer.vtk.variables.visor_variables import VisorVariables + +logger = VisorDefaultLogger(__name__) + + +class VisorSceneGraphPartNode(VisorSceneGraphNode): + """Leaf scene-graph node holding a single renderable dataset. + + Owns: + * the raw ``vtkDataObject`` + * scene-description metadata (name, VTK dataset type, bounds, + variable-array metadata) + + Does NOT own any VTK pipeline objects. The renderer looks up the + matching :class:`VtkNodePipeline` by :attr:`id`. + """ + + def __init__( + self, + parent: Self, + node_metadata: vtkInformation | None = None, + dataset: vtkDataObject | vtkUnstructuredGrid | vtkPolyData | None = None, + root_node: Self | None = None, + ): + """Initialize the part node.""" + super().__init__(parent, node_metadata, dataset, root_node) + + def _post_init( + self, + parent: Self, + node_metadata: vtkInformation | None = None, + dataset: vtkDataObject | vtkUnstructuredGrid | vtkPolyData | None = None, + root_node: Self | None = None, + ): + """Custom initialization for the part node.""" + self._set_name(node_metadata) + self._node_type = VisorNodeType.PART + + # Raw dataset -- owned here. Pipeline objects live in VtkNodePipeline. + self._dataset: vtkDataObject = dataset + + self._vtk_dataset_type: str = self._get_vtk_dataset_type(dataset) + self._bounds: list[float] = self._get_bounds(dataset) + self._variable_metadata: VisorVariables = self._get_variable_metadata(dataset) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def dataset(self) -> vtkDataObject: + """Return the raw VTK dataset.""" + return self._dataset + + @property + def data_arrays(self) -> List[dict]: + """Return the variable-array metadata for this part.""" + return self._variable_metadata.list_info() + + @property + def state(self) -> SceneGraphNodeInfo: + """Return the scene-graph node info for this part. + + Structural fields only. The base ``state`` derives ``isActorNode`` + from :attr:`is_part_node`, which is True for a part node -- the wire + payload stays byte-identical to what the legacy actor node produced. + """ + state_obj = super().state + state_obj.data_arrays = self.data_arrays + state_obj.diffuse_color = VisorColors.DefaultMeshColor + return state_obj + + # ------------------------------------------------------------------ + # Variable metadata refresh + # ------------------------------------------------------------------ + + def refresh_variable_metadata(self) -> None: + """Refresh cached variable metadata from the current dataset. + + Does not touch any VTK pipeline object. Use after an in-place + variable data update where the pipeline topology has not changed. + """ + self._variable_metadata = self._get_variable_metadata(self._dataset) + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + + def _get_vtk_dataset_type(self, dataset: vtkDataObject) -> str: + if isinstance(dataset, (UnstructuredGrid, vtkUnstructuredGrid)): + return "vtkUnstructuredGrid" + elif isinstance(dataset, (PolyData, vtkPolyData)): + return "vtkPolyData" + else: + msg = f"node type not yet supported: {type(dataset)}" + logger.error(msg) + raise RuntimeError(msg) + + def _get_bounds(self, dataset: vtkDataObject) -> list[float]: + return list(dataset.GetBounds()) + + def _get_variable_metadata(self, dataset: vtkDataObject) -> VisorVariables: + return VisorVariables(dataset) + diff --git a/src/ansys/visor/viewer/vtk/scene_graph/root_node.py b/src/ansys/visor/viewer/vtk/scene_graph/root_node.py new file mode 100644 index 00000000..c28f9918 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/scene_graph/root_node.py @@ -0,0 +1,134 @@ +"""Class representing the scene graph root node.""" + +from typing_extensions import Self +from vtkmodules.vtkCommonCore import ( + vtkInformation, +) +from vtkmodules.vtkCommonDataModel import ( + vtkDataObject, + vtkMultiBlockDataSet, + vtkMultiPieceDataSet, +) + +from ansys.visor.viewer.core.visor_enums import VisorNodeType +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.core.visor_types import VisorDatasetType +from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphGroupNode, VisorSceneGraphNode + +logger = VisorDefaultLogger(__name__) + +class VisorBounds: + """ + A class representing the bounds of a scene graph node. + """ + def __init__(self): + """Initialize the bounds.""" + self.bounds = {} + + def set(self, node_id: int, bounds: list[float]): + """Set the bounds of a scene graph node.""" + self.bounds[node_id] = bounds + + def remove(self, node_id: int): + """Remove the bounds of a scene graph node.""" + self.bounds.pop(node_id, None) + + def compute_bounds(self) -> list[float]: + """Compute the bounds of the scene graph node.""" + if not self.bounds: + return [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + # Note: used list comprehension for clarity. + # For many datasets, a single-pass loop would be more efficient. + min_x = min(bounds[0] for bounds in self.bounds.values()) + max_x = max(bounds[1] for bounds in self.bounds.values()) + min_y = min(bounds[2] for bounds in self.bounds.values()) + max_y = max(bounds[3] for bounds in self.bounds.values()) + min_z = min(bounds[4] for bounds in self.bounds.values()) + max_z = max(bounds[5] for bounds in self.bounds.values()) + return [min_x, max_x, min_y, max_y, min_z, max_z] + + +class VisorSceneGraph(VisorSceneGraphGroupNode): + """ + A root node for the scene graph that contains all other nodes. + This node serves as the entry point for the scene graph and contains all + other nodes as descendants. + """ + + def __init__(self): + """Initialize the scene graph node.""" + super().__init__(None, None, None, self) + + def _post_init(self, + parent: Self | None, + node_metadata: vtkInformation | None = None, + dataset: vtkDataObject | vtkMultiBlockDataSet | vtkMultiPieceDataSet | None = None, + root_node: Self | None = None + ): + """Custom initialization for scene graph root node.""" + self._node_type = VisorNodeType.ROOT + self._name = self._vtk_dataset_type = "root" + self._dataset_bounds: VisorBounds = VisorBounds() + + @property + def bounds(self) -> list[float]: + """ + For the root node, return the overall scene bounds computed across + all loaded datasets. + """ + return self._dataset_bounds.compute_bounds() + + def load_dataset( + self, + dataset: VisorDatasetType, + name: str | None = None, + ): + """ + Populate the scene graph with the hierarchy found in a + path to a VTK file or a pre-existing VTK dataset object. + + Args: + dataset (VisorDatasetType): The dataset object to load. + """ + + logger.debug(f"Beginning scene graph node construction from dataset: {type(dataset)}") + + info = vtkInformation() + name = "n/a" if name is None else name + info.Set(self.METADATA_NAME_KEY, name) + item = VisorSceneGraphNode.get_node(self, info, dataset, self) + # update the bounds + self._dataset_bounds.set(item.id, item.bounds) + # register the item in the node's children + self.add_child(item) + + logger.debug(f"Finished scene graph node construction from dataset: {type(dataset)}") + return item.id + + def remove_dataset(self, dataset_id: int): + """ + Remove a dataset from the scene graph by its ID. + + Args: + dataset_id (int): The ID of the dataset to remove. + """ + self.remove_node(dataset_id) + # update the bounds + self._dataset_bounds.remove(dataset_id) + logger.debug(f"Removed dataset with ID: {dataset_id}") + + def get_part_name_to_id_map(self, dataset_id: int) -> dict[str, int]: + """ + Get a mapping of part names to their corresponding node IDs + for a specific dataset. + + Args: + dataset_id (int): The ID of the dataset. + Returns: + dict[str, int]: A dictionary mapping part names to node IDs. + """ + dataset_node = self.get_descendant_node(dataset_id) + if not dataset_node: + logger.warning(f"Dataset with ID {dataset_id} not found in scene graph.") + return {} + return dataset_node.get_descendant_node_name_to_id_map() diff --git a/src/ansys/visor/viewer/vtk/variables/__init__.py b/src/ansys/visor/viewer/vtk/variables/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/vtk/variables/visor_part_variables.py b/src/ansys/visor/viewer/vtk/variables/visor_part_variables.py new file mode 100644 index 00000000..2517bce3 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/variables/visor_part_variables.py @@ -0,0 +1,35 @@ +"""VisorPartVariables: per-part variable listing returned by list_variables.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import List + +from ansys.visor.viewer.vtk.variables.visor_variables import VisorVariable + + +@dataclass(frozen=True) +class VisorPartVariables: + """ + Variables for a single part of a dataset. + + Attributes + ---------- + part_id : int + Stable runtime part ID (from PartIndex). Pass this back to + ``update_variables`` to target this specific part. + part_name : str + Human-readable block / part name. + variables : List[VisorVariable] + All point and cell variables available on this part. + """ + part_id: int + part_name: str + variables: List[VisorVariable] + + def to_dict(self) -> dict: + return { + "part_id": self.part_id, + "part_name": self.part_name, + "variables": [v.to_dict() for v in self.variables], + } + diff --git a/src/ansys/visor/viewer/vtk/variables/visor_variable_update.py b/src/ansys/visor/viewer/vtk/variables/visor_variable_update.py new file mode 100644 index 00000000..b66cc6c3 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/variables/visor_variable_update.py @@ -0,0 +1,163 @@ +"""Class for updating variables on a dataset part in Visor VTK viewer.""" + + +import itertools +from dataclasses import dataclass +from typing import Any, Sequence + +import numpy as np + +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType +from ansys.visor.viewer.core.visor_types import ArrayLike + + +@dataclass(frozen=True) +class VisorVariableUpdate: + """ + Class for updating variables on a dataset part in Visor VTK viewer. + + Attributes: + type (VisorVtkVariableType): The type of variable (POINT or CELL). + name (str): The name of the variable. + num_components (int): The number of components per data point. + data (ArrayLike): The variable data, which can be a Python list/sequence or a NumPy array. + part_id (int | None): The part ID for composite datasets; None for single-block datasets. + + Methods: + from_dict(cls, data: dict[str, Any]) -> 'VisorVariableUpdate': + Class method to create an instance from a dictionary. + _input_to_numpy_array(data: ArrayLike, num_components: int) -> np.ndarray: + Static method to convert input data to a NumPy array and validate its shape. + """ + type: VisorVtkVariableType # enum: POINT/CELL + name: str + num_components: int + data: ArrayLike + # Required for composite (multiblock) datasets; omit for single-block datasets. + part_id: int | None = None + + def __post_init__(self): + """Validate the data class attributes.""" + # Ensure type is a valid enum instance + if not isinstance(self.type, VisorVtkVariableType): + # Allow string input and coerce to enum using its canonical names + try: + object.__setattr__(self, "type", VisorVtkVariableType[self.type.upper()]) + except Exception: + raise ValueError("Invalid type for VisorVariableUpdate. Use POINT or CELL.") + # Basic sanity checks + if not self.name: + raise ValueError("name must be non-empty.") + if self.num_components <= 0: + raise ValueError("num_components must be > 0.") + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> 'VisorVariableUpdate': + """Construct an instance from a dictionary.""" + # Required fields + for key in ("type", "name", "num_components", "data"): + if key not in data: + raise ValueError(f"Missing required field: {key}") + + type_value = data["type"] # can be enum or string; conversion handled in __post_init__ + name = data["name"] + num_components_raw = data["num_components"] + data_array = data["data"] + + # Basic shape/type checks prior to constructing + try: + num_components = int(num_components_raw) + except Exception: + raise ValueError("num_components must be an integer.") + + if name is None or (isinstance(name, str) and name.strip() == ""): + raise ValueError("name must be non-empty.") + + if data_array is None: + raise ValueError("data must be provided.") + + # Convert to numpy array and enforce shape checks + arr = cls._input_to_numpy_array(data_array, num_components) + + return cls( + type=type_value, + name=name, + num_components=num_components, + data=arr, + part_id=data.get("part_id", None), + ) + + @staticmethod + def _input_to_numpy_array(data: ArrayLike, num_components: int) -> np.ndarray: + """ + Convert input data to a numpy array and validate its shape. + - Python lists/sequences: Automatically reshaped to (num_points, num_components) + - NumPy arrays: Must already have correct shape with last dimension = num_components + Args: + data (ArrayLike): Input data as a Python list/sequence or NumPy array. + num_components (int): Expected number of components per data point. + Returns: + np.ndarray: Reshaped NumPy array of shape (num_points, num_components). + Raises: + ValueError: If the input data cannot be reshaped to the expected format, + or if the number of components does not match. + """ + if not (isinstance(data, np.ndarray) or + (isinstance(data, Sequence) and not isinstance(data, (str, bytes)))): + raise TypeError("data must be a numpy.ndarray or a sequence (list, tuple, etc.)," + "not a string or bytes.") + + # --- numpy fast-path --- + if isinstance(data, np.ndarray): + if data.ndim == 1: + if num_components == 1: + # Zero-copy if already float32 C-contiguous, otherwise convert. + arr = data if (data.dtype == np.float32 and data.flags['C_CONTIGUOUS']) \ + else data.astype(np.float32) + return arr.reshape(-1, 1) + else: + raise ValueError( + f"1D NumPy array provided but num_components is {num_components}. " + "Multi-component data must be pre-shaped to (num_points, num_components), " + "or provided as a flat Python list/sequence." + ) + # 2D+ arrays must be pre-shaped + if data.shape[-1] != num_components: + raise ValueError( + f"NumPy array last dimension {data.shape[-1]} does not match " + f"num_components {num_components}. Multi-dimensional arrays must be pre-shaped." + ) + # Zero-copy if already float32 C-contiguous with the right shape. + arr = data if (data.dtype == np.float32 and data.flags['C_CONTIGUOUS']) \ + else data.astype(np.float32) + return arr.reshape(-1, num_components) + + # --- Python list/sequence fast-path --- + # For nested sequences (list-of-tuples / list-of-lists) use + # itertools.chain.from_iterable + np.fromiter โ€” ~2ร— faster than np.asarray + # because it avoids building an intermediate Python object array. + first = data[0] if len(data) > 0 else None + if first is not None and isinstance(first, (list, tuple)): + total = len(data) * num_components + arr = np.fromiter( + itertools.chain.from_iterable(data), + dtype=np.float32, + count=total, + ).reshape(-1, num_components) + return arr + + # Flat sequence โ€” standard path + arr = np.asarray(data, dtype=np.float32) + if arr.ndim == 1: + if arr.size % num_components != 0: + raise ValueError( + f"Data length {arr.size} is not divisible by num_components {num_components}." + ) + return arr.reshape(-1, num_components) + + # 2D or higher-dimensional array + if arr.shape[-1] != num_components: + raise ValueError( + f"Data last dimension {arr.shape[-1]} does not match num_components {num_components}." + ) + return arr.reshape(-1, num_components) diff --git a/src/ansys/visor/viewer/vtk/variables/visor_variables.py b/src/ansys/visor/viewer/vtk/variables/visor_variables.py new file mode 100644 index 00000000..f5fbd35b --- /dev/null +++ b/src/ansys/visor/viewer/vtk/variables/visor_variables.py @@ -0,0 +1,241 @@ +"""Class representing metadata for point and cell variables in a VTK dataset.""" + +from dataclasses import dataclass +from typing import Dict, List, Tuple + +from vtkmodules.vtkCommonCore import vtkDataArray +from vtkmodules.vtkCommonDataModel import vtkDataObject, vtkFieldData + +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.models.info.visor_variable_info import VisorVariableInfo +from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate + +logger = VisorDefaultLogger(__name__) + + +@dataclass(frozen=True) +class VisorVariable: + """ + Metadata for a VTK variable array. + + Note: This class does not manage the actual data array, only its metadata. + + Attributes: + index (int): Index of the variable within its type (point or cell). + type (VisorVtkVariableType): Type of the variable (e.g., POINT + or CELL). + name (str): Name of the variable. + num_components (int): Number of components in the variable array. + num_points (int): Number of points/cells in the variable array. + ranges (List[Tuple[float, float]]): List of ranges for each component. + magnitude_range (Tuple[float, float] | None): Range of magnitudes if applicable + """ + index: int + type: VisorVtkVariableType + name: str + num_components: int + num_points: int + ranges: List[Tuple[float, float]] + ranges: List[Tuple[float, float]] + magnitude_range: Tuple[float, float] | None = None + + @classmethod + def from_array(cls, index: int, arr: vtkDataArray, variable_type: VisorVtkVariableType) -> "VisorVariable": + """Construct a VisorVariable from the given array and variable_type.""" + name = arr.GetName() + num_components = int(arr.GetNumberOfComponents()) + num_points = int(arr.GetNumberOfTuples()) + ranges = [tuple(arr.GetRange(i)) for i in range(num_components)] + magnitude_range = tuple(arr.GetRange(-1)) if hasattr(arr, "GetRange") else None + + cls._validate_name(name) + + return cls( + index=int(index), + type=variable_type, + name=name, + num_components=num_components, + num_points=num_points, + ranges=ranges, + magnitude_range=magnitude_range, + ) + + def to_info(self): + """Return a VisorVariableInfo object from the current VisorVariable instance.""" + return VisorVariableInfo( + index_for_type=self.index, + type=self.type, + name=self.name, + num_components=self.num_components, + ranges=self.ranges, + magnitude_range=self.magnitude_range + ) + + def to_dict(self): + """Return a dictionary representation of the VisorVariable instance.""" + return { + "index": self.index, + "type": getattr(self.type, "name", str(self.type)), + "name": self.name, + "num_components": self.num_components, + "num_points": self.num_points, + "ranges": self.ranges, + "magnitude_range": self.magnitude_range, + } + + @staticmethod + def _validate_name(name: str): + """ + Validate the variable name against VTK constraints. + + Currently only validates name length, as VTK does not have strict constraints on variable names, + as the name is currently used as part of the unique identifier for the variable. + + Additional validation can be added if needed (eg. restrictions on characters etc) + """ + if name is None: + logger.warning("Variable name is None.") + return + if len(name) > 128: + logger.warning(f"Variable name '{name}' is longer than 128 chars.") + + +class VisorVariables: + """ + Metadata for point and cell variables in a VTK dataset. + This class does not manage the actual data arrays, only their metadata. + + This class only currently supports non-composite datasets, as composite datasets + require more complex handling. + + Attributes: + _point (List[VisorVariable]): List of point variable metadata. + _cell (List[VisorVariable]): List of cell variable metadata. + Methods: + reload(dataset: vtkDataObject) -> None: + Reload variable metadata from the given VTK dataset. + list_info() -> List[VisorVariableInfo]: + List metadata for all variables as VisorVariableInfo objects. + list() -> List[VisorVariable]: + List all variables as VisorVariable objects. + validate_new_variables(variables: List[VisorVariableUpdate]) -> bool: + Validate point and cell variables against existing metadata. + """ + def __init__(self, dataset: vtkDataObject): + """Initialize the variables metadata.""" + if dataset is None: + raise ValueError("Dataset cannot be None") + + # Initialize empty lists for point and cell variables + self._point: List[VisorVariable] = [] + self._cell: List[VisorVariable] = [] + + # Populate the variable metadata from the dataset + self._populate_from_dataset(dataset) + + def reload(self, dataset: vtkDataObject) -> None: + """ + Reload variable metadata from the given VTK dataset. + - `dataset` is expected to expose GetPointData() and GetCellData() methods. + """ + if dataset is None: + raise ValueError("Dataset cannot be None") + self._populate_from_dataset(dataset) + + def list_info(self) -> List[VisorVariableInfo]: + """List all variables as VisorVariableInfo objects.""" + return [var.to_info() for var in self._point + self._cell] + + def list(self) -> List[VisorVariable]: + """List all variables as VisorVariable objects.""" + return self._point + self._cell + + def validate_new_variables(self, variables: List[VisorVariableUpdate]) -> bool: + """ + Validate point and cell variables against existing metadata. + - `variables` is expected to be a mapping with optional "point" and "cell" keys, + each mapping variable names to array-like data. + + Returns True if all variables are valid, and False if any are invalid. + """ + by_type = self._group_by_type(variables) + + # Keys are (name, num_components) tuples for uniqueness + point_map: Dict[Tuple[str, int]: VisorVariable] = {(v.name, v.num_components):v for v in self._point} + cell_map: Dict[Tuple[str, int]: VisorVariable] = {(v.name, v.num_components):v for v in self._cell} + + # Validate point and cell variables separately + new_point_data = by_type.get(VisorVtkVariableType.POINT, []) + if not self._validate_for_type(point_map, new_point_data): + return False + new_cell_data = by_type.get(VisorVtkVariableType.CELL, []) + if not self._validate_for_type(cell_map, new_cell_data): + return False + + return True + + def _validate_for_type( + self, + existing_var_map: Dict[Tuple[str, int], VisorVariable], + variables_to_validate: List[VisorVariableUpdate] + ) -> bool: + """ + Validate new variables of a specific type against existing metadata. + - `new_var_dict` maps variable names to array-like data. + - `existing_metadata` is a list of VisorVariable for the variable type. + Returns True if all variables are valid, and False if any are invalid. + """ + for new_var in variables_to_validate: + if (new_var.name, new_var.num_components) not in existing_var_map: + return False # Variable name not found. + + # TODO: add check for data shape matching original data shape. + return True + + def _group_by_type( + self, + variables: List[VisorVariableUpdate] + ) -> Dict[VisorVtkVariableType, List[VisorVariableUpdate]]: + """ + Group variables by their VisorVtkVariableType. + - `variables` is a list of VisorVariableUpdate instances. + Returns a dictionary mapping VisorVtkVariableType to lists of VisorVariableUpdate. + """ + by_type: Dict[VisorVtkVariableType, List[VisorVariableUpdate]] = {} + for variable in variables: + by_type.setdefault(variable.type, []).append(variable) + return by_type + + def _populate_from_dataset(self, dataset: vtkDataObject) -> None: + """ + Populate point and cell variable metadata from a VTK dataset. + - `dataset` is expected to expose GetPointData() and GetCellData() methods. + """ + + # Clear existing metadata before repopulating + self._point.clear() + self._cell.clear() + + point_data = dataset.GetPointData() + cell_data = dataset.GetCellData() + + self._point = self._collect_variables_for_type(point_data, VisorVtkVariableType.POINT) + self._cell = self._collect_variables_for_type(cell_data, VisorVtkVariableType.CELL) + + def _collect_variables_for_type(self, data: vtkFieldData, var_type: VisorVtkVariableType) -> List[VisorVariable]: + """ + Get the list of VisorVariable metadata for the specified variable type. + - `data` is the result of GetPointData() or GetCellData() from a VTK dataset. + (vtkFieldData is the base class for both vtkPointData and vtkCellData) + - `var_type` specifies whether to get POINT or CELL variable metadata. + Returns a list of VisorVariable instances. + """ + result: List[VisorVariable] = [] + for i in range(data.GetNumberOfArrays()): + array: vtkDataArray = data.GetArray(i) + if array is None: + continue # Defensive check, should not happen + result.append(VisorVariable.from_array(i, array, var_type)) + return result + diff --git a/src/ansys/visor/viewer/vtk/widgets/__init__.py b/src/ansys/visor/viewer/vtk/widgets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/visor/viewer/vtk/widgets/visor_bounding_box.py b/src/ansys/visor/viewer/vtk/widgets/visor_bounding_box.py new file mode 100644 index 00000000..019609f7 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/widgets/visor_bounding_box.py @@ -0,0 +1,176 @@ +"""Class for creating a bounding box around a set of nodes in a VTK scene.""" + +from typing import Callable + +from trame_vtklocal.widgets.vtklocal import LocalView +from vtkmodules.vtkCommonExecutionModel import vtkPolyDataAlgorithm +from vtkmodules.vtkFiltersModeling import vtkOutlineFilter +from vtkmodules.vtkFiltersSources import vtkCubeSource +from vtkmodules.vtkRenderingAnnotation import vtkCubeAxesActor2D +from vtkmodules.vtkRenderingCore import vtkActor, vtkCamera, vtkPolyDataMapper, vtkRenderer, vtkTextProperty + +from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphNode + + +class VisorBoundingBox: + """ + Bounding box widget for a set of nodes in a VTK scene. + This class creates a bounding box around a set of nodes in a VTK scene and provides + methods to show, hide, and update the bounding box based on the nodes' bounds. + + Attributes: + update_bounds (Callable[[list[float]], None]): Function to update the bounding box bounds. + update_bounds_from_nodes (Callable[[dict[int, VisorSceneGraphNode]], None]): Function to update the bounding box bounds based on a dictionary of nodes. + show (Callable[[], None]): Function to show the bounding box. + hide (Callable[[], None]): Function to hide the bounding box. + attach_to_renderer (Callable[[vtkRenderer], None]): Function to attach the bounding box to a VTK renderer. + detach_from_renderer (Callable[[vtkRenderer], None]): Function to detach the bounding box from a VTK renderer. + toggle (Callable[[], None]): Function to toggle the visibility of the bounding box. + update_node_count (Callable[[int], None]): Function to update the node count for visibility control. + + Methods: + register_with_local_view(local_view: LocalView): Register the VTK objects with a local view for WASM integration. + + Properties: + box_algorithm_wasm_id: Returns the WASM ID of the box algorithm. Raises an error if register_with_local_view() has not been called. + outline_wasm_actor_id: Returns the WASM ID of the outline actor. Raises an error if register_with_local_view() has not been called. + axes_wasm_actor_id: Returns the WASM ID of the axes actor. Raises an error if register_with_local_view() has not been called. + """ + def __init__(self, camera: vtkCamera, node_count: int = 0): + + outline_box = vtkCubeSource() + # outline_box.SetCenter(0, 0, 0) + outline_filter = vtkOutlineFilter() + outline_filter.SetInputConnection(outline_box.GetOutputPort()) + + outline_mapper = vtkPolyDataMapper() + outline_mapper.SetInputConnection(outline_filter.GetOutputPort()) + + outline_actor = vtkActor() + outline_actor.SetMapper(outline_mapper) + outline_actor.GetProperty().SetColor(0.184, 0.427, 0.620) + + # normals = vtkPolyDataNormals() + # normals.SetInputConnection(outline_box.GetOutputPort()); + + tprop = vtkTextProperty() + tprop.SetColor(0.0, 0.0, 0.0) + tprop.ShadowOn() + tprop.SetFontSize(20) + + axes2 = vtkCubeAxesActor2D() + axes2.SetCamera(camera) + axes2.SetViewProp(outline_actor) + axes2.SetLabelFormat("%6.4g") + axes2.SetFlyModeToClosestTriad() + axes2.ScalingOff() + axes2.SetAxisTitleTextProperty(tprop) + axes2.SetAxisLabelTextProperty(tprop) + axes2.GetProperty().SetLineWidth(5) + axes2.GetProperty().SetColor(0.184, 0.427, 0.620) + + def show(): + if node_count == 0: + return + outline_actor.SetVisibility(True) + axes2.SetVisibility(True) + + def hide(): + outline_actor.SetVisibility(False) + axes2.SetVisibility(False) + + # set the default state (hidden) + hide() + + def update_node_count(count: int): + nonlocal node_count + node_count = count + + def toggle(): + if node_count == 0: + return + if outline_actor.GetVisibility(): + hide() + else: + show() + + def update_outline_bounds(bounds: list[float]): + outline_box.SetBounds(bounds) + + def update_outline_bounds_from_nodes(nodes: dict[int, VisorSceneGraphNode]): + nonlocal node_count + node_count = len(nodes) + if node_count == 0: + hide() + return + show() + x_min, x_max = float('inf'), float('-inf') + y_min, y_max = float('inf'), float('-inf') + z_min, z_max = float('inf'), float('-inf') + for node in nodes.values(): + x_min = min(x_min, node.bounds[0]) + x_max = max(x_max, node.bounds[1]) + y_min = min(y_min, node.bounds[2]) + y_max = max(y_max, node.bounds[3]) + z_min = min(z_min, node.bounds[4]) + z_max = max(z_max, node.bounds[5]) + update_outline_bounds([x_min, x_max, y_min, y_max, z_min, z_max]) + + def attach_to_renderer(renderer: vtkRenderer): + renderer.AddActor(outline_actor) + renderer.AddViewProp(axes2) + + def detach_from_renderer(renderer: vtkRenderer): + renderer.RemoveActor(outline_actor) + renderer.RemoveViewProp(axes2) + + self.update_bounds: Callable[[list[float]], None] = update_outline_bounds + self.update_bounds_from_nodes: Callable[[dict[int, VisorSceneGraphNode]], None] = update_outline_bounds_from_nodes + self.show: Callable[[], None] = show + self.hide: Callable[[], None] = hide + self.attach_to_renderer: Callable[[vtkRenderer], None] = attach_to_renderer + self.detach_from_renderer: Callable[[vtkRenderer], None] = detach_from_renderer + self.toggle: Callable[[], None] = toggle + self.update_node_count: Callable[[int], None] = update_node_count + self.__box_algorithm: vtkPolyDataAlgorithm = outline_box + self.__box_algorithm_wasm_id: int | None = None + self.__outline_actor: vtkActor = outline_actor + self.__outline_wasm_actor_id: int | None = None + self.__axes_actor: vtkCubeAxesActor2D = axes2 + self.__axes_wasm_actor_id: int | None = None + + @property + def box_algorithm_wasm_id(self): + """Return the WASM ID of the box algorithm. Raises an error if register_with_local_view() has not been called.""" + if self.__box_algorithm_wasm_id is None: + method_name = self.register_with_local_view.__name__ + raise RuntimeError(f"{method_name}() must be called before this property is available") + return self.__box_algorithm_wasm_id + + @property + def outline_wasm_actor_id(self): + """Return the WASm ID of the outline actor. Raises an error if register_with_local_view() has not been called.""" + if self.__outline_wasm_actor_id is None: + method_name = self.register_with_local_view.__name__ + raise RuntimeError(f"{method_name}() must be called before this property is available") + return self.__outline_wasm_actor_id + + @property + def axes_wasm_actor_id(self): + """Return the WASM ID of the axes actor. Raises an error if register_with_local_view() has not been called.""" + if self.__axes_wasm_actor_id is None: + method_name = self.register_with_local_view.__name__ + raise RuntimeError(f"{method_name}() must be called before this property is available") + return self.__axes_wasm_actor_id + + def register_with_local_view(self, local_view: LocalView): + """Register the VTK objects with the local view""" + box_algorithm = self.__box_algorithm + outline_actor = self.__outline_actor + axes_actor = self.__axes_actor + local_view.register_vtk_object(box_algorithm) + local_view.register_vtk_object(outline_actor) + local_view.register_vtk_object(axes_actor) + self.__box_algorithm_wasm_id = local_view.get_wasm_id(box_algorithm) + self.__outline_wasm_actor_id = local_view.get_wasm_id(outline_actor) + self.__axes_wasm_actor_id = local_view.get_wasm_id(axes_actor) diff --git a/src/ansys/visor/viewer/vtk/widgets/visor_cross_section.py b/src/ansys/visor/viewer/vtk/widgets/visor_cross_section.py new file mode 100644 index 00000000..9a28f450 --- /dev/null +++ b/src/ansys/visor/viewer/vtk/widgets/visor_cross_section.py @@ -0,0 +1,219 @@ +"""Class for creating a cross-section widget in the Visor visualizer.""" + +from typing import Callable + +from trame_vtklocal.widgets.vtklocal import LocalView +from vtkmodules.vtkCommonDataModel import vtkPlane +from vtkmodules.vtkCommonExecutionModel import vtkAlgorithm +from vtkmodules.vtkFiltersCore import vtkClipPolyData +from vtkmodules.vtkInteractionWidgets import vtkImplicitPlaneRepresentation, vtkImplicitPlaneWidget2 +from vtkmodules.vtkRenderingCore import vtkRenderWindowInteractor + +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +logger = VisorDefaultLogger(__name__) + + +class VisorCrossSectionWidget: + """ + Class for creating a cross-section widget in the Visor visualizer. + + Attributes: + _plane (vtkPlane): The VTK plane used for clipping. + _plane_representation (vtkImplicitPlaneRepresentation): The representation of the plane in the widget. + _plane_widget (vtkImplicitPlaneWidget2): The VTK widget for interacting with the plane. + _algorithm_filter (Callable[[vtkAlgorithm], vtkAlgorithm]): A callable that applies the clipping algorithm to a given VTK algorithm. + _plane_wasm_id (int | None): The WebAssembly ID for the plane object, if registered with a local view. + _plane_widget_wasm_id (int | None): The WebAssembly ID for the plane widget, if registered with a local view. + _plane_representation_wasm_id (int | None): The WebAssembly ID for the plane representation, if registered with a local view. + _place_factor (float): The factor applied to the plane representation. + _inside_out (bool): Whether or not the plane is inside out or not. + + Methods: + update_bounds(initial_bounds): Updates the bounding box around the given VTK bounding box. + register_with_local_view(local_view): Registers the VTK objects with a local view and assigns their WebAssembly IDs. + set_normal(normal): Sets the normal vector of the plane. + set_origin(origin): Sets the origin point of the plane. + toggle_on_off(): Toggles the visibility of the plane widget on or off. + turn_on(): Turns on the plane widget. + turn_off(): Turns off the plane widget. + + Properties: + plane: Returns the VTK plane object. + algorithm_filter: Returns the VTK algorithm filter. + plane_wasm_id: Returns the WebAssembly ID for the plane object. + plane_widget_wasm_id: Returns the WebAssembly ID for the plane widget. + plane_representation_wasm_id: Returns the WebAssembly ID for the plane representation. + is_on: Returns whether the plane widget is enabled or not. + """ + _plane: vtkPlane + _plane_representation: vtkImplicitPlaneRepresentation + _plane_widget: vtkImplicitPlaneWidget2 + _algorithm_filter: Callable[[vtkAlgorithm], vtkAlgorithm] + _plane_wasm_id: int | None + _plane_widget_wasm_id: int | None + _plane_representation_wasm_id: int | None + + def __init__(self, window_interactor: vtkRenderWindowInteractor): + """""" + self._place_factor: float = 1.25 + self._inside_out: bool = False + self._plane_wasm_id = None + self._plane_widget_wasm_id = None + self._plane_representation_wasm_id = None + # VTK objects will be initialized later in _initialize_cross_section_widget() + + logger.info("Beginning creating cross-section widget") + try: + # Initialize the plane and its representation + self._plane = self._initialize_plane() + self._plane_representation = self._initialize_plane_representation() + + # Set initial bounds + initial_bounds: list[float] = [0, 0, 0, 0, 0, 0] + self.update_bounds(initial_bounds) + + # Initialize the plane widget + self._plane_widget = self._initialize_plane_widget(window_interactor) + + # Define the algorithm filter function + def algorithm_filter(algorithm: vtkAlgorithm) -> vtkAlgorithm: + clipper = vtkClipPolyData() + clipper.SetClipFunction(self._plane) + clipper.SetInsideOut(1 if self._inside_out else 0) + clipper.SetInputConnection(algorithm.GetOutputPort()) + return clipper + + # Assign the algorithm filter + self._algorithm_filter = algorithm_filter + logger.info("Finished creating cross-section widget") + except RuntimeError as e: + msg = f"could not create cross-section widget: {e}" + logger.error(msg) + raise RuntimeError(msg) + + def update_bounds(self, bounds: list[float]): + """Updates the bounding box around the given VTK bounding box.""" + if not hasattr(self, "_plane_representation"): + raise RuntimeError("_plane_representation must be initialized before calling update_bounds()") + self._plane_representation.PlaceWidget(bounds) + (o_x, o_y, o_z, n_x, n_y, n_z) = self._get_default_plane_info(bounds) + # oZ = bounds[4] + ((bounds[5] - bounds[4]) / 2) + self.set_origin((o_x, o_y, o_z)) + self.set_normal((n_x, n_y, n_z)) + + def _get_default_plane_info(self, bounds: list[float]) -> ( + tuple[float, float, float, float, float, float] + ): + """ + This function calculates an origin's coordinates from a given bounding box + such that the origin is placed at one end of the bounding box's z-axis or + the other depending on whether the clip function is inside out or not. + """ + x = 0.5 * (bounds[0] + bounds[1]) + y = 0.5 * (bounds[2] + bounds[3]) + z = 0.5 * (bounds[4] + bounds[5]) + a = (bounds[4] - z) * self._place_factor + return x, y, (z + (-a if self._inside_out else a)), 0, 0, 1 + + @property + def plane(self) -> vtkPlane: + """Returns the widget VTK plane object.""" + return self._plane + + @property + def algorithm_filter(self): + """Returns the VTK algorithm filter.""" + return self._algorithm_filter + + @property + def plane_wasm_id(self): + """Returns the VTK plane wasm id.""" + if self._plane_wasm_id is None: + method_name = self.register_with_local_view.__name__ + raise RuntimeError(f"{method_name}() must be called before this property is available") + return self._plane_wasm_id + + @property + def plane_widget_wasm_id(self): + """Returns the VTK plane widget WASM ID.""" + if self._plane_widget_wasm_id is None: + method_name = self.register_with_local_view.__name__ + raise RuntimeError(f"{method_name}() must be called before this property is available") + return self._plane_widget_wasm_id + + @property + def plane_representation_wasm_id(self): + """Returns the VTK plane representation WASM ID.""" + if self._plane_representation_wasm_id is None: + method_name = self.register_with_local_view.__name__ + raise RuntimeError(f"{method_name}() must be called before this property is available") + return self._plane_representation_wasm_id + + @property + def is_on(self): + """Returns whether the plane widget is enabled or not.""" + return self._plane_widget.GetEnabled() == 1 + + def register_with_local_view(self, local_view: LocalView): + """Registers the VTK objects with a local view and assigns their WebAssembly IDs.""" + self._plane_wasm_id = local_view.register_vtk_object(self._plane) + self._plane_widget_wasm_id = local_view.register_vtk_object(self._plane_widget) + self._plane_representation_wasm_id = local_view.register_vtk_object(self._plane_representation) + + def set_normal(self, normal: tuple[float, float, float]): + """Set the normal of the widget plane.""" + self._plane_representation.SetNormal(normal[0], normal[1], normal[2]) + self._plane.SetNormal(normal[0], normal[1], normal[2]) + + def set_origin(self, origin: tuple[float, float, float]): + """Set the origin of the widget plane.""" + self._plane_representation.SetOrigin(origin[0], origin[1], origin[2]) + self._plane.SetOrigin(origin[0], origin[1], origin[2]) + + def toggle_on_off(self): + """toggle the widget on or off.""" + if self.is_on: + self.turn_off() + else: + self.turn_on() + + def turn_on(self): + """Turn the widget on.""" + self._plane_widget.On() + + def turn_off(self): + """Turn the widget off.""" + self._plane_widget.Off() + + # Private methods + def _initialize_plane(self): + """ + Initialize the plane for the cross-section widget. + """ + return vtkPlane() + + def _initialize_plane_representation(self): + """ + Initialize the plane representation for the cross-section widget. + """ + if not hasattr(self, "_plane"): + raise RuntimeError("_plane must be initialized before creating _plane_representation") + rep = vtkImplicitPlaneRepresentation() + rep.SetPlaceFactor(self._place_factor) + rep.SetOutlineTranslation(False) + rep.normal = self._plane.normal + return rep + + def _initialize_plane_widget(self, window_interactor: vtkRenderWindowInteractor) -> vtkImplicitPlaneWidget2: + """ + Initialize the plane widget for the cross-section widget. + """ + if not hasattr(self, "_plane_representation"): + raise RuntimeError("_plane_representation must be initialized before creating _plane_widget") + plane_widget = vtkImplicitPlaneWidget2() + plane_widget.SetInteractor(window_interactor) + plane_widget.SetRepresentation(self._plane_representation) + plane_widget.Off() + return plane_widget + diff --git a/src/ansys/visor/viewer/vtk/widgets/visor_orientation.py b/src/ansys/visor/viewer/vtk/widgets/visor_orientation.py new file mode 100644 index 00000000..206b864e --- /dev/null +++ b/src/ansys/visor/viewer/vtk/widgets/visor_orientation.py @@ -0,0 +1,60 @@ +"""Class for creating an orientation widget in the Visor visualizer.""" + + +from trame_vtklocal.widgets.vtklocal import LocalView +from vtkmodules.vtkInteractionWidgets import vtkCameraOrientationRepresentation, vtkCameraOrientationWidget +from vtkmodules.vtkRenderingCore import vtkRenderer, vtkRenderWindowInteractor + +from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger + +logger = VisorDefaultLogger(__name__) + + +class VisorOrientationWidget: + """ + Class for creating an orientation widget in the Visor visualizer. + + Attributes: + __widget (vtkCameraOrientationWidget): The VTK camera orientation widget. + __widget_wasm_id (int | None): The ID of the widget in the local view, or None if not registered. + + Methods: + register_with_local_view(local_view) + + Properties: + is_on (bool): Whether the widget is on or not. + register_with_local_view(local_view) + """ + def __init__(self, renderer: vtkRenderer, window_interactor: vtkRenderWindowInteractor): + """Initialize the orientation widget.""" + try: + logger.info("Beginning creating orientation widget") + widget = vtkCameraOrientationWidget(parent_renderer=renderer, interactor=window_interactor) + representation: vtkCameraOrientationRepresentation = widget.GetRepresentation() + representation.AnchorToLowerLeft() + widget.On() + logger.info("Finished creating orientation widget") + except RuntimeError as e: + msg = f"could not create orientation widget: {e}" + logger.error(msg) + raise RuntimeError(msg) + + self.__widget: vtkCameraOrientationWidget = widget + self.__widget_wasm_id: int | None = None + + @property + def is_on(self): + """Return whether the widget is on or not.""" + return self.__widget.GetEnabled() == 1 + + def register_with_local_view(self, local_view: LocalView): + """Register the widget with the local view.""" + self.__widget_wasm_id = local_view.register_vtk_object(self.__widget) + + @property + def widget_wasm_id(self): + """Return the widget WASM ID.""" + if self.__widget_wasm_id is None: + method_name = self.register_with_local_view.__name__ + raise RuntimeError(f"{method_name}() must be called before this property is available") + return self.__widget_wasm_id diff --git a/src/ansys/visor/visor-client/.gitignore b/src/ansys/visor/visor-client/.gitignore new file mode 100644 index 00000000..adca247f --- /dev/null +++ b/src/ansys/visor/visor-client/.gitignore @@ -0,0 +1,125 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.venvd \ No newline at end of file diff --git a/src/ansys/visor/visor-client/.prettierignore b/src/ansys/visor/visor-client/.prettierignore new file mode 100644 index 00000000..c60cc4b4 --- /dev/null +++ b/src/ansys/visor/visor-client/.prettierignore @@ -0,0 +1,4 @@ +dist/ +build/ +node_modules/ +*.min.js diff --git a/src/ansys/visor/visor-client/.prettierrc b/src/ansys/visor/visor-client/.prettierrc new file mode 100644 index 00000000..2e42cd22 --- /dev/null +++ b/src/ansys/visor/visor-client/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/src/ansys/visor/visor-client/assets/react.svg b/src/ansys/visor/visor-client/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/src/ansys/visor/visor-client/assets/react.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> \ No newline at end of file diff --git a/src/ansys/visor/visor-client/babel.config.json b/src/ansys/visor/visor-client/babel.config.json new file mode 100644 index 00000000..7b936a40 --- /dev/null +++ b/src/ansys/visor/visor-client/babel.config.json @@ -0,0 +1,16 @@ +{ + "presets": [ + ["@babel/preset-env", {}], + [ + "@babel/preset-react", + { + // "runtime": "automatic" ensures that we don't + // have to import "React" in every .tsx file that + // Jest analyzes. + // see https://stackoverflow.com/a/69834932 + "runtime": "automatic" + } + ], + ["@babel/preset-typescript", {}] + ] +} diff --git a/src/ansys/visor/visor-client/css-stub-for-jest.js b/src/ansys/visor/visor-client/css-stub-for-jest.js new file mode 100644 index 00000000..5a60a5fe --- /dev/null +++ b/src/ansys/visor/visor-client/css-stub-for-jest.js @@ -0,0 +1,4 @@ +// This solves the SyntaxError: Unexpected token ':' error +// when Jest tests try to import CSS. +// see https://stackoverflow.com/a/39434579 +module.exports = {}; diff --git a/src/ansys/visor/visor-client/eslint.config.js b/src/ansys/visor/visor-client/eslint.config.js new file mode 100644 index 00000000..ccd75809 --- /dev/null +++ b/src/ansys/visor/visor-client/eslint.config.js @@ -0,0 +1,42 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['dist'], + }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'react-hooks/exhaustive-deps': [ + // Set this rule to 'off' to ensure that providing an empty + // array [] as useEffect()'s second argument does not generate + // a warning. The call to useEffect() for some components must + // NEVER EVER run twice, so we provide the empty array []. To + // ensure that devs don't think that providing an empty array + // is a bug and then feel the need to change it in the future, + // we disable this warning. + 'off', + ], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-this-alias': 'off', + }, + } +); diff --git a/src/ansys/visor/visor-client/files/plate.vtp b/src/ansys/visor/visor-client/files/plate.vtp new file mode 100644 index 00000000..c9f669dc Binary files /dev/null and b/src/ansys/visor/visor-client/files/plate.vtp differ diff --git a/src/ansys/visor/visor-client/index.html b/src/ansys/visor/visor-client/index.html new file mode 100644 index 00000000..c4655b80 --- /dev/null +++ b/src/ansys/visor/visor-client/index.html @@ -0,0 +1,16 @@ +<!doctype html> +<html lang="en" class="light"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Visor + + + +
+ + + diff --git a/src/ansys/visor/visor-client/jest.config.cjs b/src/ansys/visor/visor-client/jest.config.cjs new file mode 100644 index 00000000..50703231 --- /dev/null +++ b/src/ansys/visor/visor-client/jest.config.cjs @@ -0,0 +1,16 @@ +/** @type {import('jest').Config} */ +const config = { + // 'jsdom' ensures that Jest recognizes + // the JavaScript "document" object + // see https://stackoverflow.com/a/67845294 + testEnvironment: 'jsdom', + verbose: true, + moduleNameMapper: { + // This solves the SyntaxError: Unexpected token ':' error + // when Jest tests try to import CSS. + // see https://stackoverflow.com/a/39434579 + '\\.(css|less|sass|scss)$': '/css-stub-for-jest.js', + }, +}; + +module.exports = config; diff --git a/src/ansys/visor/visor-client/package-lock.json b/src/ansys/visor/visor-client/package-lock.json new file mode 100644 index 00000000..2424fd82 --- /dev/null +++ b/src/ansys/visor/visor-client/package-lock.json @@ -0,0 +1,9434 @@ +{ + "name": "visor-client", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "visor-client", + "version": "0.2.0", + "dependencies": { + "@kitware/trame": "^1.0.0", + "@kitware/vtk-wasm": "1.7.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.28.5", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@eslint/js": "^9.0.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/jest": ">=30.0.0", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.0.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.0", + "globals": "^17.4.0", + "jest": ">=30.3.0", + "jest-environment-jsdom": ">=30.3.0", + "jest-junit": "^16.0.0", + "prettier": "^3.0.0", + "typescript": "^6.0.2", + "typescript-eslint": "^8.0.0", + "vite": "^8.0.3" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.11.tgz", + "integrity": "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", + "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kitware/trame": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@kitware/trame/-/trame-1.0.0.tgz", + "integrity": "sha512-WP5hG7hR/JpaERL+1RvAv1fPl5n3tqVZoDDY0QXDdbgQmDaEgf4b1g+WtETeXX7iHL69AYPUA5lJxp05SPULAQ==", + "license": "MIT", + "dependencies": { + "@kitware/wslink": "^2.5.5" + } + }, + "node_modules/@kitware/vtk-wasm": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@kitware/vtk-wasm/-/vtk-wasm-1.7.3.tgz", + "integrity": "sha512-jVAfScC5JLDqNsyOR9vQrKIsijWhAJ0hzOrP4gisSGBxA/mGtjJ2WobtrxgkkhIR+njTSNN+Crsekls/VIQaZA==", + "license": "Apache-2.0", + "dependencies": { + "js-untar": "^2.0.0", + "jszip": "3.10.1" + } + }, + "node_modules/@kitware/wslink": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@kitware/wslink/-/wslink-2.5.6.tgz", + "integrity": "sha512-/wX0UGEOC51AJ0e82Rj8B+sdr4nYxsnx7lYAEYAm+Aahkf0PW47o5w8qol6P9yIw9oUEX8HUJ73O5bwjjwnd5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@msgpack/msgpack": "^2.8.0" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", + "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", + "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", + "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.328", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", + "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-junit/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-untar": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/js-untar/-/js-untar-2.0.0.tgz", + "integrity": "sha512-7CsDLrYQMbLxDt2zl9uKaPZSdmJMvGGQ7wo9hoB3J+z/VcO2w63bXFgHVnjF1+S9wD3zAu8FBVj7EYWjTQ3Z7g==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.3.tgz", + "integrity": "sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/ansys/visor/visor-client/package.json b/src/ansys/visor/visor-client/package.json new file mode 100644 index 00000000..2e952dea --- /dev/null +++ b/src/ansys/visor/visor-client/package.json @@ -0,0 +1,48 @@ +{ + "name": "visor-client", + "private": true, + "version": "0.2.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "preview": "vite preview", + "test": "jest" + }, + "dependencies": { + "@kitware/trame": "^1.0.0", + "@kitware/vtk-wasm": "1.7.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.28.5", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@eslint/js": "^9.0.0", + "eslint": "^9.0.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.0", + "typescript-eslint": "^8.0.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/jest": ">=30.0.0", + "jest-junit": "^16.0.0", + "@vitejs/plugin-react": "^6.0.1", + "globals": "^17.4.0", + "jest": ">=30.3.0", + "jest-environment-jsdom": ">=30.3.0", + "typescript": "^6.0.2", + "vite": "^8.0.3", + "prettier": "^3.0.0" + }, + "overrides": { + "picomatch": ">=4.0.4" + } +} diff --git a/src/ansys/visor/visor-client/public/css/theme-dark.css b/src/ansys/visor/visor-client/public/css/theme-dark.css new file mode 100644 index 00000000..0ec5ff74 --- /dev/null +++ b/src/ansys/visor/visor-client/public/css/theme-dark.css @@ -0,0 +1,13 @@ +:root { + --theme-color-1: 28, 30, 35; + --theme-color-2: 34, 39, 45; + --theme-color-3: 42, 48, 55; + --theme-color-borders: 121, 121, 129; + --theme-color-text: 240, 240, 240; + --theme-color-selected-background: 42, 48, 55; + --theme-color-selected-text: 240, 240, 240; + /* Visor-specific settings below */ + --visor-viewer-background: 54, 60, 67; + --visor-legend-overlay-opacity: 0.6; + --visor-legend-overlay-text-color: 255, 255, 255; +} diff --git a/src/ansys/visor/visor-client/public/css/theme-light.css b/src/ansys/visor/visor-client/public/css/theme-light.css new file mode 100644 index 00000000..c76fd08a --- /dev/null +++ b/src/ansys/visor/visor-client/public/css/theme-light.css @@ -0,0 +1,13 @@ +:root { + --theme-color-1: 255, 255, 255; + --theme-color-2: 234, 234, 240; + --theme-color-3: 216, 216, 222; + --theme-color-borders: 121, 121, 129; + --theme-color-text: 53, 53, 53; + --theme-color-selected-background: 233, 244, 254; + --theme-color-selected-text: 33, 150, 243; + /* Visor-specific settings below */ + --visor-viewer-background: 247, 247, 247; + --visor-legend-overlay-opacity: 0.2; + --visor-legend-overlay-text-color: 0, 0, 0; +} diff --git a/src/ansys/visor/visor-client/public/favicon.ico b/src/ansys/visor/visor-client/public/favicon.ico new file mode 100644 index 00000000..1c33ad23 Binary files /dev/null and b/src/ansys/visor/visor-client/public/favicon.ico differ diff --git a/src/ansys/visor/visor-client/public/fonts/source-sans-3.woff2 b/src/ansys/visor/visor-client/public/fonts/source-sans-3.woff2 new file mode 100644 index 00000000..864cc417 Binary files /dev/null and b/src/ansys/visor/visor-client/public/fonts/source-sans-3.woff2 differ diff --git a/src/ansys/visor/visor-client/public/requestInterceptor.js b/src/ansys/visor/visor-client/public/requestInterceptor.js new file mode 100644 index 00000000..cbe1ea48 --- /dev/null +++ b/src/ansys/visor/visor-client/public/requestInterceptor.js @@ -0,0 +1,37 @@ +/* + * THIS SERVICE WORKER IS REGISTERED IN WasmView.js + */ +const sw = /**@type{ServiceWorkerGlobalScope}*/ self; +sw.onactivate = (e) => { + // When a service worker is initially registered, pages won't + // use it until their next load. The claim() method causes + // those pages to be controlled immediately. Be aware that + // this results in your service worker controlling pages that + // loaded regularly over the network, or possibly via a + // different service worker. + // see https://stackoverflow.com/a/68773653 + e.waitUntil(self.clients.claim()); +}; +sw.onfetch = (e) => { + e.respondWith( + /**@type{PromiseLike}*/ ( + async () => { + let request = /**@type{Request}*/ e.request; + // Ensure Trame's request to "/paraview" returns a success code. If we do not do this, + // there will be an error message logged in the browser developer console stating + // that "/paraview" returned a 404. The request to "/paraview" is not needed for Visor + // to function. + if (/paraview\/*$/i.test(request.url) && /^post$/i.test(request.method)) { + return new Response('', { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + }); + } + // Ensure we always fetch resources anew, do not cache them. + // This mitigates strange bugs that may occur if a user's browser + // decides to cache old versions of trame-vtklocal or the mjs/wasm files. + return await fetch(request, { cache: 'no-store' }); + } + )() + ); +}; diff --git a/src/ansys/visor/visor-client/public/vite.svg b/src/ansys/visor/visor-client/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/src/ansys/visor/visor-client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ansys/visor/visor-client/src/App.tsx b/src/ansys/visor/visor-client/src/App.tsx new file mode 100644 index 00000000..2fb10ec6 --- /dev/null +++ b/src/ansys/visor/visor-client/src/App.tsx @@ -0,0 +1,214 @@ +import { UiScaffold, UiScaffoldUtil } from './components/UiScaffold.tsx'; +import { CSSProperties, RefObject, useEffect, useRef, useState } from 'react'; +import { VisorFrontend } from './VisorFrontend.tsx'; +import VisorSceneDetails, { + SchemaVersionMismatchError, +} from './state/appstate/VisorSceneDetails.tsx'; +import VisorAppState from './state/appstate/VisorAppState.tsx'; +import RemoteVtkScene from './wasm/RemoteVtkScene'; +import { WasmRenderer } from './renderer/WasmRenderer'; +import { requireWasmAnnotation } from './renderer/RendererAnnotation'; +import { randomInt } from './utils/JsHelpers'; + +type VisorArgs = { + host: string; + port: number; + aspectRatio: number; + pixelDensity: number; + darkMode: boolean; + basePath: string; +}; + +function App() { + const visorArgs: RefObject = useRef(null!); + if (visorArgs.current == null) { + // Get current args object from window. + // If the args object was not found on + // the window, create a new object. + const args = (visorArgs.current = (window as any).__visorArgs ?? {}); + // Delete args object from window if it existed there. + // BHB update 2025-11-21: don't delete the args object here, + // as they might actually need to be preserved across App.tsx remounts. + // Need to investigate. + // delete (window as any).__visorArgs; + // Set default values for args if any were not set. + args.host ??= location.hostname; + if (args.port == null) { + if (location.port === '') { + args.port = location.protocol === 'https:' ? 443 : 80; + } else { + args.port = parseInt(location.port); + } + } + args.aspectRatio ??= 800 / 600; + args.pixelDensity ??= 1000; + args.basePath ??= ''; + // Sanity-check each argument's value. + if ((typeof args.host as any) !== 'string' || args.host === '') { + throw new Error(`host must be a non-empty string`); + } else if ((typeof args.port as any) !== 'number' || args.port <= 0) { + throw new Error(`port must be a number greater than 0`); + } else if ((typeof args.aspectRatio as any) !== 'number' || args.aspectRatio <= 0) { + throw new Error(`aspectRatio must be a number greater than 0`); + } else if ((typeof args.pixelDensity as any) !== 'number' || args.pixelDensity <= 0) { + throw new Error(`pixelDensity must be a number greater than 0`); + } + } + const visorContainerStyle: CSSProperties = + visorArgs.current.aspectRatio > 0 + ? { + position: 'relative', + aspectRatio: `${visorArgs.current.aspectRatio}`, + maxHeight: '100%', + margin: '0 auto', + } + : { + position: 'relative', + minHeight: '100px', + width: `100%`, + height: `100%`, + margin: '0 auto', + }; + const uiScaffoldUtil: RefObject = useRef(null!); + const [visorState, setVisorFrontend] = useState(null!); + const [schemaMismatchError, setSchemaMismatchError] = + useState(null); + const wasmView = useRef(null); + + useEffect(() => { + (async () => { + if (wasmView.current == null) { + const { host, port } = visorArgs.current; + const secure = window.location.protocol === 'https:'; + wasmView.current = await RemoteVtkScene.getInstanceAsync({ + wasmIdsStateKey: 'wasm_ids', + refNameStateKey: 'wasm_ref_name', + wasmUrl: `http${secure ? 's' : ''}://${host}:${port}/wasm`, + webSocketUrl: `ws${secure ? 's' : ''}://${host}:${port}/ws`, + }); + wasmView.current.addServerUpdatedListener(onServerUpdateAsync); + await onServerUpdateAsync(); + return; + } else if (uiScaffoldUtil.current == null) { + throw new Error(`uiScaffoldUtil.current should not be null here`); + } + (window as any).__visorState = visorState; + ////////////////////////////////////////////////////////////////////////// + // Set UiScaffold params. + uiScaffoldUtil.current.pixelDensity = visorArgs.current.pixelDensity; + })(); + return () => { + // Cleanup + delete (window as any).__visorState; + }; + }, [visorState]); + + async function onServerUpdateAsync() { + if (wasmView.current == null) { + throw new Error(`wasmView should not be null here`); + } + const perfLogging = wasmView.current.trameGetState('perf_logging') === true; + const t0 = perfLogging ? performance.now() : 0; + if (perfLogging) console.log(`[PERF] onServerUpdateAsync start`); + + wasmView.current.clearObserversAndEventListeners(); + const oldFrontend: VisorFrontend = (window as any).__visorState; + const sceneDetailsJson = await wasmView.current.trameTriggerAsync( + 'get_visor_scene_details_json' + ); + let sceneDetails: VisorSceneDetails; + try { + sceneDetails = new VisorSceneDetails(sceneDetailsJson); + } catch (err) { + if (err instanceof SchemaVersionMismatchError) { + setSchemaMismatchError(err); + return; + } + throw err; + } + const renderer = await WasmRenderer.createAsync( + wasmView.current.vtkScene, + requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation) + ); + const newFrontend = new VisorFrontend(renderer, sceneDetails.vtkInfo.sceneGraph); + if (visorArgs.current.darkMode != null) { + // Explicit Dash prop takes precedence over the server's dark_mode value. + sceneDetails.appState.ui.setDarkTheme(visorArgs.current.darkMode); + } + (window as any).__visorState = newFrontend; + await newFrontend.setAppStateAsync(sceneDetails.appState, false); + setVisorFrontend(newFrontend); // rebuild/remount React UI + wasmView.current.addServerUpdatedListener(onServerUpdateAsync); + wasmView.current.addGetStateListener(onGetState); + wasmView.current.addSetStateListener(onSetStateAsync); + if (oldFrontend != null) { + const oldAppState = await oldFrontend.getAppStateAsync(); + await newFrontend.setAppStateAsync(oldAppState); + } + const view = wasmView.current; + setTimeout(() => { + view.vtkScene.resizeAsync(); + }, 100); + if (perfLogging) { + const handlerMs = performance.now() - t0; + console.log(`[PERF] onServerUpdateAsync total=${handlerMs.toFixed(2)}ms`); + wasmView.current + .trameTriggerAsync('perf_report_server_update', { handlerMs }) + .catch(() => {}); + } + } + + async function onGetState(payload: any) { + if (wasmView.current == null) { + throw new Error(`wasmView should not be null here`); + } + const oldFrontend: VisorFrontend = (window as any).__visorState; + const appState = await oldFrontend.getAppStateAsync(); + const response = { + requestId: payload.requestId, + appState: appState.toDict(), + }; + await wasmView.current.trameTriggerAsync( + 'save_state_response', + payload.requestId, + response + ); + } + + async function onSetStateAsync(payload: any) { + if (wasmView.current == null) { + throw new Error(`wasmView should not be null here`); + } + const newAppState = new VisorAppState(payload?.appState); + if (visorArgs.current.darkMode != null) { + // Explicit Dash prop takes precedence over the theme stored in saved state. + newAppState.ui.setDarkTheme(visorArgs.current.darkMode); + } + const oldFrontend: VisorFrontend = (window as any).__visorState; + await oldFrontend.setAppStateAsync(newAppState, true); + } + + return ( +
+ {schemaMismatchError != null ? ( +
+ {`Scene details schema version mismatch: payload is version ` + + `${schemaMismatchError.payloadVersion}, client expects version ` + + `${schemaMismatchError.expectedVersion}.`} +
+ ) : ( + visorState != null && ( + { + uiScaffoldUtil.current = u; + }} + /> + ) + )} +
+ ); +} + +export default App; diff --git a/src/ansys/visor/visor-client/src/VisorFrontend.tsx b/src/ansys/visor/visor-client/src/VisorFrontend.tsx new file mode 100644 index 00000000..a2cc9e19 --- /dev/null +++ b/src/ansys/visor/visor-client/src/VisorFrontend.tsx @@ -0,0 +1,540 @@ +import { getSpectrumManager, VisorSpectrumCollection } from './state/VisorSpectrumManager.tsx'; +import { CreateVisorSceneGraph, VisorSceneNodeExtended } from './state/VisorSceneGraph.tsx'; +import { getPromiseResolver } from './utils/JsHelpers'; +import VisorAppState from './state/appstate/VisorAppState.tsx'; +import VisorDatasetState from './state/appstate/VisorDatasetState.tsx'; +import VisorPartState from './state/appstate/VisorPartState.tsx'; +import VisorSpectrumState from './state/appstate/VisorSpectrumState.tsx'; +import { TreeViewUtil } from './treeview/TreeView.tsx'; +import { StateInput } from './state/appstate/VisorStateCommon.tsx'; +import VisorVtkSceneNode from './state/appstate/vtkInfo/VisorVtkSceneNode.tsx'; +import { IRenderer, VisorCameraState } from './renderer/IRenderer'; +import { Panel_TopRight_Util } from './components/ui-panels/Panel_TopRight_Util.tsx'; +import { Panel_TopLeft_Util } from './components/ui-panels/Panel_TopLeft_Util.tsx'; +import { OrientationWidget } from './widgets/orientationWidget.ts'; +import { UiScaffoldUtil } from './components/UiScaffold.tsx'; + +export type { VisorCameraState } from './renderer/IRenderer'; + +export class VisorFrontend { + constructor(renderer: IRenderer, sceneGraphNode: VisorVtkSceneNode) { + const spectrumManager = getSpectrumManager(); + const sceneGraph = CreateVisorSceneGraph(sceneGraphNode, spectrumManager, renderer); + spectrumManager.finishAddingDataArrayMetadata(); + renderer.attachSceneGraph(sceneGraph); + + let treeViewUtilSet: boolean = false; + let panelTopLeftUtilSet: boolean = false; + let panelTopRightUtilSet: boolean = false; + let uiScaffoldUtilSet: boolean = false; + const { promise: treeViewUtilPromise, resolver: treeViewUtilResolve } = + getPromiseResolver>(); + const { + isSet: isPanelTopLeftUtilSet, + promise: panelTopLeftUtilPromise, + resolver: panelTopLeftUtilResolve, + } = getPromiseResolver(); + const { + isSet: isPanelTopRightUtilSet, + promise: panelTopRightUtilPromise, + resolver: panelTopRightUtilResolve, + } = getPromiseResolver(); + const { + isSet: isUiScaffoldUtilSet, + promise: uiScaffoldUtilPromise, + resolver: uiScaffoldUtilResolve, + } = getPromiseResolver(); + let darkMode: boolean = true; + + // TODO: uncomment these lines when the dataset addition bug is fixed + // const orientationWidget = vtkScene.getVtkObject(vtkInfo.orientationWidgetWasmId); + // use "void" here to suppress the "no await" IDE warning + // void orientationWidget.SetShouldResetCamera(false); + + // const orientationWidget = new OrientationWidget(vtkScene, vtkInfo.orientationWidgetWasmId); + + let themeDarkCss: string | null | undefined = null; + let themeLightCss: string | null | undefined = null; + let themeStyleElem: HTMLStyleElement | null | undefined = null; + + // Respect base path prefix set by the Dash component (for reverse-proxy deployments). + const bp: string = ((window as any).__visorArgs?.basePath ?? '').replace(/\/$/, ''); + + const self = this; + this.globalSpectrumCollection = spectrumManager.globalSpectrumCollection; + this.#unit = ''; + this.darkMode = darkMode; + this.render = async () => { + await renderer.renderAsync(); + }; + this.resizeAsync = async () => { + await renderer.resizeAsync(); + // await orientationWidget.resizeAsync(); + }; + this.domElement = renderer.domElement; + this.getCameraStateAsync = () => renderer.getCameraStateAsync(); + this.defaultActorColor = []; + this.setSpectrumRangeAsync = async (spectrumId, component, min, max) => { + const spectrum = spectrumManager.globalSpectrumCollection.getSpectrum(spectrumId); + if (spectrum == null) { + return; + } + spectrum.setCustomRange(component, min, max); + for (const actorNode of sceneGraph.descendantActorNodesOrSelfArray) { + if ( + actorNode.spectrumId === spectrumId && + actorNode.spectrumComponent == component + ) { + await actorNode.setScalarRangeAsync(min, max); + } + } + }; + this.addCameraChangedListener = (callback) => { + return renderer.addCameraChangedListener(callback); + }; + this.addFrameRenderedListener = (callback) => { + return renderer.addFrameRenderedListener((fps) => { + callback(fps); + }); + }; + this.addViewerClickedListener = (callback) => { + return renderer.addViewerClickedListener( + (nodeId, ctrlKey, shiftKey, _normX, _normY) => { + const node = + nodeId == null + ? null + : sceneGraph.descendantActorNodesOrSelfDictionary[nodeId]; + callback(node ?? null, ctrlKey, shiftKey); + } + ); + }; + + // Selection mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + type SelectionMode = 'part' | 'vertex' | 'edge' | 'face'; + let selectionMode: SelectionMode = 'part'; + const selectionModeChangedListeners = new Map<() => void, (mode: SelectionMode) => void>(); + const geometryPickedListeners = new Map<() => void, (result: any) => void>(); + + this.getSelectionMode = () => selectionMode; + this.setSelectionMode = (mode: SelectionMode) => { + selectionMode = mode; + // Clear the client-side highlight overlay whenever mode changes + renderer.clearGeometryHighlight(); + for (const cb of selectionModeChangedListeners.values()) cb(mode); + }; + this.addSelectionModeChangedListener = (callback) => { + const remover = () => selectionModeChangedListeners.delete(remover); + selectionModeChangedListeners.set(remover, callback); + return remover; + }; + this.addGeometryPickedListener = (callback) => { + const remover = () => geometryPickedListeners.delete(remover); + geometryPickedListeners.set(remover, callback); + return remover; + }; + this.pickGeometryAsync = async (normX: number, normY: number) => { + const mode = selectionMode; + if (mode === 'part') return null; + const result = await renderer.pickGeometryAsync(normX, normY, mode); + // Draw client-side highlight overlay (no server sync needed) + renderer.setGeometryHighlightAsync(result).catch(() => {}); + for (const cb of geometryPickedListeners.values()) cb(result); + return result; + }; + + // Extended viewer click listener that also triggers geometry picking + renderer.addViewerClickedListener((_nodeId, _ctrlKey, _shiftKey, normX, normY) => { + if (selectionMode !== 'part') { + this.pickGeometryAsync(normX, normY).catch(() => {}); + } + }); + this.sceneGraph = sceneGraph; + this.treeViewUtilPromise = treeViewUtilPromise; + this.panelTopLeftUtilPromise = panelTopLeftUtilPromise; + this.panelTopRightUtilPromise = panelTopRightUtilPromise; + this.uiScaffoldUtilPromise = uiScaffoldUtilPromise; + this.setTreeViewUtil = (treeViewUtil) => { + if (treeViewUtil == null) { + throw new Error('treeViewUtil cannot be null'); + } else if (treeViewUtilSet) { + throw new Error('treeViewUtil has already been set'); + } + treeViewUtilSet = true; + treeViewUtilResolve(treeViewUtil); + }; + this.setPanelTopLeftUtil = (panelTopLeftUtil) => { + if (panelTopLeftUtil == null) { + throw new Error('panelTopLeftUtil cannot be null'); + } else if (panelTopLeftUtilSet) { + throw new Error('panelTopLeftUtil has already been set'); + } + panelTopLeftUtilSet = true; + panelTopLeftUtilResolve(panelTopLeftUtil); + }; + this.setPanelTopRightUtil = (panelTopRightUtil) => { + if (panelTopRightUtil == null) { + throw new Error('panelTopRightUtil cannot be null'); + } else if (panelTopRightUtilSet) { + throw new Error('panelTopRightUtil has already been set'); + } + panelTopRightUtilSet = true; + panelTopRightUtilResolve(panelTopRightUtil); + }; + this.setUiScaffoldUtil = (uiScaffoldUtil) => { + if (uiScaffoldUtil == null) { + throw new Error('uiScaffoldUtil cannot be null'); + } else if (uiScaffoldUtilSet) { + throw new Error('uiScaffoldUtil has already been set'); + } + uiScaffoldUtilSet = true; + uiScaffoldUtilResolve(uiScaffoldUtil); + }; + this.toggleFullScreenAsync = () => renderer.toggleFullScreenAsync(); + this.setEdgeVisibilityAsync = (visible) => renderer.setEdgeVisibilityGlobalAsync(visible); + this.setCrossSectionVisibilityAsync = (visible) => + renderer.setCrossSectionVisibilityAsync(visible); + this.updateCrossSectionBoundsAsync = () => renderer.updateCrossSectionBoundsAsync(); + this.setBoundingBoxVisibilityAsync = (visible) => + renderer.setBoundingBoxVisibilityAsync(visible); + this.updateBoundingBoxBoundsAsync = () => renderer.updateBoundingBoxBoundsAsync(); + this.setOrthographicModeAsync = async () => { + await renderer.setOrthographicModeAsync(); + const uiScaffold = await uiScaffoldUtilPromise; + uiScaffold.setUnit(this.#unit); + }; + this.getAppStateAsync = async (keyedByName) => { + const appState = new VisorAppState(); + const uiState = appState.ui; + uiState.setDarkTheme(darkMode); + if (isPanelTopLeftUtilSet()) { + const panelTopLeft = await panelTopLeftUtilPromise; + uiState.setPanelTopLeftPanelCollapsed(panelTopLeft.isPanelCollapsed); + } + if (isPanelTopRightUtilSet()) { + const panelTopRight = await panelTopRightUtilPromise; + uiState.setPanelTopRightPanelCollapsed(panelTopRight.isPanelCollapsed); + uiState.setPanelTopRightLegendCollapsed(panelTopRight.isLegendCollapsed); + uiState.setPanelTopRightTabIndex(panelTopRight.tabIndex); + } + const sceneState = appState.scene; + sceneState.setUnit(this.#unit); + sceneState.setOrthographicEnabled(renderer.isOrthographicEnabled()); + sceneState.setCrossSectionEnabled(renderer.isCrossSectionVisible()); + sceneState.setEdgesEnabled(renderer.areEdgesVisibleGlobally()); + sceneState.setBoundingBoxEnabled(renderer.isBoundingBoxVisible()); + const currentCameraState = await renderer.getCameraStateAsync(); + sceneState.camera.setPosition(currentCameraState.position as number[]); + sceneState.camera.setFocalPoint(currentCameraState.focalPoint as number[]); + sceneState.camera.setViewUp(currentCameraState.viewUp as number[]); + sceneState.camera.setClippingRange(currentCameraState.clippingRange as number[]); + sceneState.camera.setParallelProjection(currentCameraState.parallelProjection); + sceneState.camera.setViewAngle(currentCameraState.viewAngle); + sceneState.camera.setParallelScale(currentCameraState.parallelScale); + sceneState.crossSection.setOrigin( + (await renderer.getCrossSectionOriginAsync()) as number[] + ); + sceneState.crossSection.setNormal( + (await renderer.getCrossSectionNormalAsync()) as number[] + ); + for (const datasetNode of sceneGraph.children) { + const datasetState = new VisorDatasetState(); + datasetState.setId(datasetNode.id.toString()); + datasetState.setKeyedByName(keyedByName); + datasetState.setName(datasetNode.name); + for (const partNode of datasetNode.descendantActorNodesOrSelfArray) { + const partState = new VisorPartState(); + partState.setId(partNode.id.toString()); + partState.setKeyedByName(keyedByName); + partState.setName(partNode.name); + partState.setVisible(partNode.visible); + partState.setDiffuseRgb(partNode.diffuseRgb); + partState.setOpacity(partNode.opacity); + partState.setSelected(partNode.selected); + partState.setSpectrumId(partNode.spectrumId); + partState.setSpectrumComponent(partNode.spectrumComponent); + datasetState.copyPart(partState); + } + sceneState.copyDataset(datasetState); + } + const spectrumInfos = spectrumManager.globalSpectrumCollection; + for (const spectrumInfo of spectrumInfos.array) { + const spectrumState = new VisorSpectrumState(); + spectrumState.setId(spectrumInfo.id.toString()); + const magnitudeRange = spectrumInfo.getRangeInfo(-1); + if (magnitudeRange == null) { + throw new Error(`range at component ${-1} not found`); + } + spectrumState.setMagnitudeRange(magnitudeRange.customRange); + const ranges: number[][] = []; + for (let i = 0; i < spectrumInfo.numComponents; i++) { + const range = spectrumInfo.getRangeInfo(i); + if (range == null) { + throw new Error(`range at component ${i} not found`); + } + ranges.push(range.customRange); + } + spectrumState.setRanges(ranges); + sceneState.copySpectrum(spectrumState); + } + return appState; + }; + this.setAppStateAsync = async (state, updateUI) => { + const appState = new VisorAppState(state); + const uiState = appState.ui; + if (uiState.darkTheme !== undefined) { + darkMode = uiState.darkTheme; + if (themeDarkCss == null) { + const response = await fetch(`${bp}/css/theme-dark.css`); + themeDarkCss = await response.text(); + } + if (themeLightCss == null) { + const response = await fetch(`${bp}/css/theme-light.css`); + themeLightCss = await response.text(); + } + // Ensure the theme style element exists + // and is a child of the element. + if (themeStyleElem == null) { + const styleElemId = `__visorThemeStyle`; + themeStyleElem = document.getElementById(styleElemId) as HTMLStyleElement; + if (themeStyleElem == null) { + themeStyleElem = document.createElement('style'); + themeStyleElem.id = styleElemId; + } + } + if (darkMode) { + themeStyleElem.innerHTML = themeDarkCss; + } else { + themeStyleElem.innerHTML = themeLightCss; + } + if (themeStyleElem.parentNode !== document.head) { + document.head.appendChild(themeStyleElem); + } + } + void (async () => { + const panelTopLeft = await panelTopLeftUtilPromise; + if (uiState.panelTopLeftPanelCollapsed !== undefined) { + if (uiState.panelTopLeftPanelCollapsed) { + panelTopLeft.collapsePanel(); + } else { + panelTopLeft.expandPanel(); + } + } + })(); + void (async () => { + const panelTopRight = await panelTopRightUtilPromise; + if (uiState.panelTopRightPanelCollapsed !== undefined) { + if (uiState.panelTopRightPanelCollapsed) { + panelTopRight.collapsePanel(); + } else { + panelTopRight.expandPanel(); + } + } + if (uiState.panelTopRightLegendCollapsed !== undefined) { + if (uiState.panelTopRightLegendCollapsed) { + panelTopRight.collapseLegend(); + } else { + panelTopRight.expandLegend(); + } + } + if (uiState.panelTopRightTabIndex !== undefined) { + panelTopRight.selectTab(uiState.panelTopRightTabIndex); + } + })(); + const selectedNodeIds: number[] = []; + const promises: Promise[] = []; + const sceneState = appState.scene; + if (sceneState.unit !== undefined) { + const unit = (this.#unit = sceneState.unit); + void (async () => { + const uiScaffold = await uiScaffoldUtilPromise; + uiScaffold.setUnit(unit); + })(); + } + if (sceneState.orthographicEnabled !== undefined) { + const promise = renderer.setOrthographicModeAsync(sceneState.orthographicEnabled); + promises.push(promise); + } + if (sceneState.crossSectionEnabled !== undefined) { + const promise = renderer.setCrossSectionVisibilityAsync( + sceneState.crossSectionEnabled + ); + promises.push(promise); + } + if (sceneState.edgesEnabled !== undefined) { + const promise = renderer.setEdgeVisibilityGlobalAsync(sceneState.edgesEnabled); + promises.push(promise); + } + if (sceneState.boundingBoxEnabled !== undefined) { + const promise = renderer.setBoundingBoxVisibilityAsync( + sceneState.boundingBoxEnabled + ); + promises.push(promise); + } + const cameraState = sceneState.camera; + if (cameraState.position !== undefined) { + const promise = renderer.setCameraPositionAsync(cameraState.position); + promises.push(promise); + } + if (cameraState.focalPoint !== undefined) { + const promise = renderer.setCameraFocalPointAsync(cameraState.focalPoint); + promises.push(promise); + } + if (cameraState.viewUp !== undefined) { + const promise = renderer.setCameraViewUpAsync(cameraState.viewUp); + promises.push(promise); + } + if (cameraState.clippingRange !== undefined) { + const promise = renderer.setCameraClippingRangeAsync(cameraState.clippingRange); + promises.push(promise); + } + if (cameraState.parallelProjection !== undefined) { + const promise = renderer.setCameraParallelProjectionAsync( + cameraState.parallelProjection + ); + promises.push(promise); + } + if (cameraState.viewAngle !== undefined) { + const promise = renderer.setCameraViewAngleAsync(cameraState.viewAngle); + promises.push(promise); + } + if (cameraState.parallelScale !== undefined) { + const promise = renderer.setCameraParallelScaleAsync(cameraState.parallelScale); + promises.push(promise); + } + const crossSectionState = sceneState.crossSection; + if (crossSectionState.origin !== undefined) { + const promise = renderer.setCrossSectionOriginAsync(crossSectionState.origin); + promises.push(promise); + } + if (crossSectionState.normal !== undefined) { + const promise = renderer.setCrossSectionNormalAsync(crossSectionState.normal); + promises.push(promise); + } + for (const dataset_state of sceneState.getDatasetStates()) { + for (const part_state of dataset_state.getPartStates()) { + let node; + if (part_state.keyedByName) { + node = + sceneGraph.descendantActorNodesOrSelfDictionaryByPartName[ + part_state.id + ]; + } else { + node = sceneGraph.descendantActorNodesOrSelfDictionary[part_state.id]; + } + if (node != null) { + if (part_state.selected === true) { + const idNumber = parseInt(part_state.id); + isNaN(idNumber) || selectedNodeIds.push(idNumber); + } + if (part_state.diffuseRgb !== undefined) { + const [r, g, b] = part_state.diffuseRgb; + const promise = node.setDiffuseColorRgbAsync(r, g, b); + promises.push(promise); + } + if (part_state.opacity !== undefined) { + const promise = node.setOpacityAsync(part_state.opacity); + promises.push(promise); + } + if (part_state.visible !== undefined) { + const promise = node.setVisibilityAsync(part_state.visible); + promises.push(promise); + } + if ( + part_state.spectrumId !== undefined && + part_state.spectrumComponent !== undefined + ) { + let promise; + if (part_state.spectrumId !== null) { + promise = node.setColorVariableAsync( + part_state.spectrumId, + part_state.spectrumComponent + ); + } else { + promise = node.clearColorVariableAsync(); + } + promises.push(promise); + } + } + } + } + // wait for all the parts to be updated before updating the spectrum ranges + await Promise.all(promises); + promises.length = 0; + for (const spectrum_state of sceneState.getSpectrumStates()) { + const idStr = spectrum_state.id; + if (spectrum_state.magnitudeRange !== undefined) { + const range = spectrum_state.magnitudeRange; + const promise = self.setSpectrumRangeAsync(idStr, -1, range[0], range[1]); + promises.push(promise); + } + for (let i = 0; i < spectrum_state.ranges.length; i++) { + const range = spectrum_state.ranges[i]; + if (range !== undefined) { + const promise = self.setSpectrumRangeAsync(idStr, i, range[0], range[1]); + promises.push(promise); + } + } + } + await Promise.all(promises); + if (updateUI) { + await panelTopRightUtilPromise; + const treeViewUtil: TreeViewUtil = + await treeViewUtilPromise; + treeViewUtil.setSelection(selectedNodeIds); + treeViewUtil.synchronize(); + } + await renderer.resizeAsync(); + }; + Object.freeze(this); + } + + get unit() { + return this.#unit; + } + + #unit: string; + darkMode: boolean; + render: () => Promise; + resizeAsync: () => Promise; + sceneGraph: VisorSceneNodeExtended; + domElement: HTMLDivElement; + setTreeViewUtil: (treeViewUtil: TreeViewUtil) => void; + setPanelTopLeftUtil: (panelTopRightUtil: Panel_TopLeft_Util) => void; + setPanelTopRightUtil: (panelTopRightUtil: Panel_TopRight_Util) => void; + setUiScaffoldUtil: (uiScaffoldUtil: UiScaffoldUtil) => void; + treeViewUtilPromise: Promise>; + panelTopLeftUtilPromise: Promise; + panelTopRightUtilPromise: Promise; + uiScaffoldUtilPromise: Promise; + getCameraStateAsync: () => Promise; + toggleFullScreenAsync: () => Promise; + addCameraChangedListener: (callback: (cameraState: VisorCameraState) => void) => () => void; + globalSpectrumCollection: VisorSpectrumCollection; + setSpectrumRangeAsync: ( + spectrumId: string, + component: number, + min: number, + max: number + ) => Promise; + defaultActorColor: number[]; + addViewerClickedListener: ( + handler: (node: VisorSceneNodeExtended | null, ctrlKey: boolean, shiftKey: boolean) => void + ) => () => void; + addFrameRenderedListener: (handler: (fps: number) => void) => () => void; + setCrossSectionVisibilityAsync: (visible?: boolean) => Promise; + updateCrossSectionBoundsAsync: () => Promise; + setBoundingBoxVisibilityAsync: (visible?: boolean) => Promise; + setEdgeVisibilityAsync: (visible?: boolean | undefined) => Promise; + updateBoundingBoxBoundsAsync: () => Promise; + setOrthographicModeAsync: (enable?: boolean) => Promise; + getAppStateAsync: (keyedByName?: boolean) => Promise; + setAppStateAsync: (state?: StateInput, updateUI?: boolean) => Promise; + // Selection mode + getSelectionMode: () => 'part' | 'vertex' | 'edge' | 'face'; + setSelectionMode: (mode: 'part' | 'vertex' | 'edge' | 'face') => void; + addSelectionModeChangedListener: ( + callback: (mode: 'part' | 'vertex' | 'edge' | 'face') => void + ) => () => void; + addGeometryPickedListener: (callback: (result: any) => void) => () => void; + pickGeometryAsync: (normX: number, normY: number) => Promise; +} diff --git a/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx b/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx new file mode 100644 index 00000000..33196888 --- /dev/null +++ b/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx @@ -0,0 +1,273 @@ +import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import { AggregateSpectrumInfo } from './AggregateSpectrumInfo.tsx'; +import { tryParseFloat } from '../utils/JsHelpers'; + +/** + * Event handlers emitted when aggregate spectrum selections change. + */ +class AggregateSelectionEvents { + /** + * Called when the selected spectrum changes. + * + * A value of `undefined` generally represents a mixed selection, while + * `null` represents no selected spectrum. + */ + onSpectrumChange!: ((spectrumInfo: AggregateSpectrumInfo | null | undefined) => void) | null; + + /** + * Called when a component of the selected spectrum changes. + * + * A value of `undefined` generally represents a mixed selection, while + * `null` represents no selected spectrum. + */ + onSpectrumComponentChange!: + ((spectrumInfo: AggregateSpectrumInfo | null | undefined) => void) | null; +} + +/** + * Represents the shared editable state of a collection of selected scene nodes. + * + * Properties contain a concrete value when all selected nodes share that + * value. A value of `undefined` indicates that the selected nodes have + * different values, while `null` indicates that the value is not set. + * + * Instances must be created with {@link AggregateSelectionInfo.getInstanceAsync}. + */ +export class AggregateSelectionInfo { + /** + * Prevents direct construction. + * + * Use {@link AggregateSelectionInfo.getInstanceAsync} instead. + */ + private constructor() {} + + /** + * Display name shared by the selected nodes. + */ + #displayName: string | null | undefined = null; + + /** + * Display opacity shared by the selected nodes. + */ + #displayOpacity: number | null | undefined = null; + + /** + * Spectrum identifier shared by the selected nodes. + */ + #displaySpectrumId: string | null | undefined = null; + + /** + * Custom diffuse color shared by the selected nodes, represented as a + * hexadecimal color string. + */ + #displayDiffuseColor: string | null | undefined = null; + + /** + * Available spectra collected from all selected nodes, keyed by spectrum ID. + */ + #spectrumOptions: Map = new Map(); + + /** + * Aggregate information for the currently selected spectrum. + */ + #currentSpectrumInfo: AggregateSpectrumInfo | null | undefined = null; + + /** + * Event handlers associated with this aggregate selection. + */ + #events: AggregateSelectionEvents = { + onSpectrumChange: null, + onSpectrumComponentChange: null, + }; + + /** + * Creates aggregate selection information for a collection of scene nodes. + * + * The resulting display properties contain a shared value when every node + * has the same value. Properties are set to `undefined` when the nodes have + * differing values. + * + * Spectrum options from all supplied nodes are also collected and converted + * into {@link AggregateSpectrumInfo} instances. + * + * @param actorNodes - Scene nodes included in the aggregate selection. + * @returns A promise resolving to the initialized aggregate selection. + */ + static async getInstanceAsync( + actorNodes: VisorSceneNodeExtended[] + ): Promise { + const obj = new AggregateSelectionInfo(); + + for (let i = 0; i < actorNodes.length; i++) { + const node = actorNodes[i]; + const thisName: string | null = node.name; + const thisOpacity: number | null = node.opacity; + const thisSpectrumId: string | null = node.spectrumId; + const thisDiffuseColor: string | null = node.customDiffuseColorHex; + + for (const spectrumInfo of node.spectrumCollection.array) { + const idStr = spectrumInfo.id.toString(); + + if (!obj.#spectrumOptions.has(idStr)) { + const val = await AggregateSpectrumInfo.getInstanceAsync( + actorNodes, + obj, + spectrumInfo + ); + obj.#spectrumOptions.set(idStr, val); + } + } + + if (i === 0) { + obj.#displayName = thisName; + obj.#displayOpacity = thisOpacity; + obj.#displaySpectrumId = thisSpectrumId; + obj.#displayDiffuseColor = thisDiffuseColor; + } else { + obj.#displayName !== thisName && (obj.#displayName = undefined); + + obj.#displayOpacity !== thisOpacity && (obj.#displayOpacity = undefined); + + obj.#displaySpectrumId !== thisSpectrumId && (obj.#displaySpectrumId = undefined); + + obj.#displayDiffuseColor !== thisDiffuseColor && + (obj.#displayDiffuseColor = undefined); + } + } + + if (obj.#displaySpectrumId != null) { + obj.#currentSpectrumInfo = obj.#spectrumOptions.get(obj.#displaySpectrumId.toString()); + + if (obj.#currentSpectrumInfo == null) { + console.warn(`invalid spectrumId: '${obj.#displaySpectrumId}'`); + } + } else { + obj.#currentSpectrumInfo = obj.#displaySpectrumId; + } + + return obj; + } + + /** + * Gets the display name shared by the selected nodes. + * + * @returns The shared name, `null` when unset, or `undefined` when mixed. + */ + get displayName(): string | null | undefined { + return this.#displayName; + } + + /** + * Gets the display opacity shared by the selected nodes. + * + * @returns The shared opacity, `null` when unset, or `undefined` when mixed. + */ + get displayOpacity(): number | null | undefined { + return this.#displayOpacity; + } + + /** + * Gets the custom diffuse color shared by the selected nodes. + * + * @returns The shared hexadecimal color, `null` when unset, or `undefined` + * when mixed. + */ + get displayDiffuseColor(): string | null | undefined { + return this.#displayDiffuseColor; + } + + /** + * Gets the spectrum identifier shared by the selected nodes. + * + * @returns The shared spectrum ID, `null` when unset, or `undefined` when + * mixed. + */ + get displaySpectrumId(): string | null | undefined { + return this.#displaySpectrumId; + } + + /** + * Gets information about the currently selected spectrum. + * + * @returns The current spectrum information, `null` when no valid spectrum + * is selected, or `undefined` when the selection is mixed. + */ + get currentSpectrumInfo(): AggregateSpectrumInfo | null | undefined { + return this.#currentSpectrumInfo; + } + + /** + * Gets all available aggregate spectrum options. + * + * @returns A map of spectrum IDs to aggregate spectrum information. + */ + get spectrumOptions(): ReadonlyMap { + return this.#spectrumOptions; + } + + /** + * Gets the event handlers for this aggregate selection. + * + * @returns The aggregate selection event handlers. + */ + get events(): AggregateSelectionEvents { + return this.#events; + } + + /** + * Updates the aggregate display name. + * + * @param name - The new display name. Use `null` to clear the name or + * `undefined` to represent a mixed selection. + */ + setDisplayName = (name: string | null | undefined): void => { + this.#displayName = name; + }; + + /** + * Updates the aggregate display opacity. + * + * String values are parsed as floating-point numbers by + * {@link tryParseFloat}. + * + * @param opacity - The new numeric opacity, a parseable string, `null`, or + * `undefined`. + */ + setDisplayOpacity = (opacity: number | string | null | undefined): void => { + this.#displayOpacity = tryParseFloat(opacity); + }; + + /** + * Selects a spectrum by its numeric or string identifier. + * + * When the identifier is not present in {@link spectrumOptions}, the + * current spectrum and display spectrum ID are set to `null`. Passing + * `null` or `undefined` preserves that value and clears or marks the + * selection as mixed, respectively. + * + * The `onSpectrumChange` handler is invoked after the selection is updated. + * + * @param id - The spectrum identifier to select, `null` to clear the + * selection, or `undefined` to represent a mixed selection. + * @returns Information about the selected spectrum, `null` when no matching + * spectrum exists, or `undefined` for a mixed selection. + */ + setDisplaySpectrumId = ( + id: number | string | null | undefined + ): AggregateSpectrumInfo | null | undefined => { + if (id != null) { + this.#currentSpectrumInfo = this.#spectrumOptions.get(id.toString()) ?? null; + + this.#displaySpectrumId = this.#currentSpectrumInfo?.id ?? null; + } else { + this.#currentSpectrumInfo = id; + this.#displaySpectrumId = id; + } + + if (this.#events.onSpectrumChange != null) { + this.#events.onSpectrumChange(this.#currentSpectrumInfo); + } + + return this.#currentSpectrumInfo; + }; +} diff --git a/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumComponentInfo.tsx b/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumComponentInfo.tsx new file mode 100644 index 00000000..d1a2eeb1 --- /dev/null +++ b/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumComponentInfo.tsx @@ -0,0 +1,248 @@ +import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import { VisorSpectrumComponentMetadata } from '../state/VisorSpectrumManager.tsx'; +import { AggregateSpectrumInfo } from './AggregateSpectrumInfo.tsx'; +import { tryParseFloat } from '../utils/JsHelpers'; + +/** + * Metadata describing a spectrum component within an aggregate spectrum. + */ +type SpectrumComponentMetadata = VisorSpectrumComponentMetadata & {}; + +/** + * Event callbacks emitted when the component's displayed spectrum range changes. + */ +class AggregateSpectrumComponentInfoEvents { + /** + * Called when the displayed minimum value changes. + * + * `undefined` may indicate that selected actors have different minimum + * values or that range information is unavailable. + */ + onMinChange!: ((min: number | null | undefined) => void) | null; + + /** + * Called when the displayed maximum value changes. + * + * `undefined` may indicate that selected actors have different maximum + * values or that range information is unavailable. + */ + onMaxChange!: ((max: number | null | undefined) => void) | null; +} + +/** + * Represents the aggregate display-range state for one spectrum component + * across a collection of scene nodes. + * + * A range value has the following meanings: + * + * - `number`: all selected nodes share that value. + * - `null`: the shared range endpoint has not been explicitly set. + * - `undefined`: the selected nodes have different values, or range + * information is unavailable for at least one node. + * + * Instances must be created with {@link AggregateSpectrumComponentInfo.getInstanceAsync}. + */ +export class AggregateSpectrumComponentInfo { + /** + * Prevents direct construction. + * + * Use {@link AggregateSpectrumComponentInfo.getInstanceAsync} to create + * and initialize an instance. + */ + private constructor() {} + + /** Aggregate spectrum associated with this component. */ + #spectrumInfo!: AggregateSpectrumInfo; + + /** Metadata describing this spectrum component. */ + #componentMetadata!: SpectrumComponentMetadata; + + /** Shared default minimum across the selected scene nodes. */ + #displaySpectrumDefaultMin: number | null | undefined = null; + + /** Shared default maximum across the selected scene nodes. */ + #displaySpectrumDefaultMax: number | null | undefined = null; + + /** Shared custom minimum across the selected scene nodes. */ + #displaySpectrumMin: number | null | undefined = null; + + /** Shared custom maximum across the selected scene nodes. */ + #displaySpectrumMax: number | null | undefined = null; + + /** Event callbacks for changes to the custom display range. */ + #events: AggregateSpectrumComponentInfoEvents = { + onMinChange: null, + onMaxChange: null, + }; + + /** + * Creates aggregate component information for the supplied scene nodes. + * + * Range endpoints are retained when every node has the same value. + * An endpoint is set to `undefined` when values differ between nodes or + * when a node does not contain range information for this component. + * + * @param actorNodes - Scene nodes whose spectrum ranges will be aggregated. + * @param spectrumInfo - Aggregate spectrum containing the component. + * @param componentMetadata - Metadata identifying the spectrum component. + * @returns A fully initialized aggregate component information instance. + */ + static async getInstanceAsync( + actorNodes: VisorSceneNodeExtended[], + spectrumInfo: AggregateSpectrumInfo, + componentMetadata: SpectrumComponentMetadata + ): Promise { + const obj = new AggregateSpectrumComponentInfo(); + obj.#spectrumInfo = spectrumInfo; + obj.#componentMetadata = componentMetadata; + + for (let i = 0; i < actorNodes.length; i++) { + const node = actorNodes[i]; + const thisSpectrum = node.spectrumCollection.getSpectrum(spectrumInfo.id); + const rangeInfo = thisSpectrum?.getRangeInfo(componentMetadata.id); + + if (rangeInfo == null) { + obj.#displaySpectrumDefaultMin = undefined; + obj.#displaySpectrumDefaultMax = undefined; + obj.#displaySpectrumMin = undefined; + obj.#displaySpectrumMax = undefined; + break; + } + + const { defaultRange, customRange } = rangeInfo; + const thisSpectrumDefaultMin: number | null = defaultRange[0]; + const thisSpectrumDefaultMax: number | null = defaultRange[1]; + const thisSpectrumMin: number | null = customRange[0]; + const thisSpectrumMax: number | null = customRange[1]; + + if (i === 0) { + obj.#displaySpectrumDefaultMin = thisSpectrumDefaultMin; + obj.#displaySpectrumDefaultMax = thisSpectrumDefaultMax; + obj.#displaySpectrumMin = thisSpectrumMin; + obj.#displaySpectrumMax = thisSpectrumMax; + } else { + obj.#displaySpectrumDefaultMin !== thisSpectrumDefaultMin && + (obj.#displaySpectrumDefaultMin = undefined); + + obj.#displaySpectrumDefaultMax !== thisSpectrumDefaultMax && + (obj.#displaySpectrumDefaultMax = undefined); + + obj.#displaySpectrumMin !== thisSpectrumMin && + (obj.#displaySpectrumMin = undefined); + + obj.#displaySpectrumMax !== thisSpectrumMax && + (obj.#displaySpectrumMax = undefined); + } + } + + return obj; + } + + /** + * Gets the spectrum component identifier. + */ + get id(): SpectrumComponentMetadata['id'] { + return this.#componentMetadata.id; + } + + /** + * Gets the metadata associated with this spectrum component. + */ + get metadata(): SpectrumComponentMetadata { + return this.#componentMetadata; + } + + /** + * Gets the aggregate spectrum associated with this component. + */ + get spectrumInfo(): AggregateSpectrumInfo { + return this.#spectrumInfo; + } + + /** + * Gets the shared default minimum value. + * + * @returns The shared value, `null` when unset, or `undefined` when the + * selected nodes do not agree or range information is unavailable. + */ + get displaySpectrumDefaultMin(): number | null | undefined { + return this.#displaySpectrumDefaultMin; + } + + /** + * Gets the shared default maximum value. + * + * @returns The shared value, `null` when unset, or `undefined` when the + * selected nodes do not agree or range information is unavailable. + */ + get displaySpectrumDefaultMax(): number | null | undefined { + return this.#displaySpectrumDefaultMax; + } + + /** + * Gets the current custom minimum value. + * + * @returns The shared value, `null` when unset, or `undefined` when the + * selected nodes do not agree or range information is unavailable. + */ + get displaySpectrumMin(): number | null | undefined { + return this.#displaySpectrumMin; + } + + /** + * Gets the current custom maximum value. + * + * @returns The shared value, `null` when unset, or `undefined` when the + * selected nodes do not agree or range information is unavailable. + */ + get displaySpectrumMax(): number | null | undefined { + return this.#displaySpectrumMax; + } + + /** + * Gets the event callbacks for display-range changes. + */ + get events(): AggregateSpectrumComponentInfoEvents { + return this.#events; + } + + /** + * Parses and sets the custom minimum display value. + * + * The registered {@link AggregateSpectrumComponentInfoEvents.onMinChange} + * callback is invoked after the value is updated. + * + * @param min - A numeric value, numeric string, `null`, or `undefined`. + * @returns `true` when the parsed value is neither `null` nor `undefined`; + * otherwise `false`. + */ + setDisplaySpectrumMin = (min: number | string | null | undefined): boolean => { + this.#displaySpectrumMin = tryParseFloat(min); + + if (this.#events.onMinChange != null) { + this.#events.onMinChange(this.#displaySpectrumMin); + } + + return this.#displaySpectrumMin != null; + }; + + /** + * Parses and sets the custom maximum display value. + * + * The registered {@link AggregateSpectrumComponentInfoEvents.onMaxChange} + * callback is invoked after the value is updated. + * + * @param max - A numeric value, numeric string, `null`, or `undefined`. + * @returns `true` when the parsed value is neither `null` nor `undefined`; + * otherwise `false`. + */ + setDisplaySpectrumMax = (max: number | string | null | undefined): boolean => { + this.#displaySpectrumMax = tryParseFloat(max); + + if (this.#events.onMaxChange != null) { + this.#events.onMaxChange(this.#displaySpectrumMax); + } + + return this.#displaySpectrumMax != null; + }; +} diff --git a/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumInfo.tsx b/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumInfo.tsx new file mode 100644 index 00000000..a9bcffe5 --- /dev/null +++ b/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumInfo.tsx @@ -0,0 +1,210 @@ +import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import { VisorSpectrumInfo } from '../state/VisorSpectrumManager.tsx'; +import { AggregateSpectrumComponentInfo } from './AggregateSpectrumComponentInfo.tsx'; +import { AggregateSelectionInfo } from './AggregateSelectionInfo.tsx'; + +/** + * Represents aggregate spectrum information for a collection of selected scene + * nodes. + * + * The class resolves the available spectrum components and determines whether + * the selected nodes share a common component to display. + * + * Instances must be created with {@link AggregateSpectrumInfo.getInstanceAsync}. + */ +export class AggregateSpectrumInfo { + /** + * Creates an uninitialized aggregate spectrum information object. + * + * @private + */ + private constructor() {} + + /** Metadata for the spectrum represented by this instance. */ + #spectrumInfo!: VisorSpectrumInfo; + + /** Selection state associated with this spectrum. */ + #selectionInfo!: AggregateSelectionInfo; + + /** + * The component ID currently displayed by all selected nodes. + * + * - A number indicates that all nodes share the same component. + * - `null` indicates that no component is selected. + * - `undefined` indicates that the selected nodes have different components. + */ + #displayComponentId: number | null | undefined = null; + + /** + * Information for the currently displayed component. + * + * This follows the same nullability semantics as + * {@link AggregateSpectrumInfo.displayComponentId}. + */ + #currentComponentInfo: AggregateSpectrumComponentInfo | null | undefined = null; + + /** + * Available component information, indexed by the component ID converted to + * a string. + */ + #componentOptions: Map = new Map(); + + /** + * Creates and initializes aggregate spectrum information for a collection + * of scene nodes. + * + * Component metadata is resolved asynchronously. The selected nodes are then + * inspected to determine whether they share a common spectrum component. + * + * When a node's configured component is unavailable for the spectrum, the + * first component option is used as its fallback. + * + * @param actorNodes - Scene nodes included in the aggregate selection. + * @param selectionInfo - Selection state that owns this spectrum information. + * @param spectrumInfo - Metadata describing the spectrum and its components. + * @returns A fully initialized aggregate spectrum information instance. + */ + static async getInstanceAsync( + actorNodes: VisorSceneNodeExtended[], + selectionInfo: AggregateSelectionInfo, + spectrumInfo: VisorSpectrumInfo + ): Promise { + const obj = new AggregateSpectrumInfo(); + obj.#selectionInfo = selectionInfo; + obj.#spectrumInfo = spectrumInfo; + + for (const componentMetadata of spectrumInfo.componentOptions) { + const val = await AggregateSpectrumComponentInfo.getInstanceAsync( + actorNodes, + obj, + componentMetadata + ); + obj.#componentOptions.set(componentMetadata.id.toString(), val); + } + + for (let i = 0; i < actorNodes.length; i++) { + const node = actorNodes[i]; + const spectrum = node.spectrumCollection.getSpectrum(spectrumInfo.id); + let thisComponentId: number | null = node.spectrumComponent; + + if (spectrum != null) { + if (spectrum.getRangeInfo(node.spectrumComponent) == null) { + // Default to the first component option. + thisComponentId = spectrum.componentOptions[0].id; + } + } + + if (i === 0) { + obj.#displayComponentId = thisComponentId; + } else { + obj.#displayComponentId !== thisComponentId && + (obj.#displayComponentId = undefined); + } + } + + if (obj.#displayComponentId != null) { + obj.#currentComponentInfo = obj.#componentOptions.get( + obj.#displayComponentId.toString() + ); + + if (obj.#currentComponentInfo == null) { + console.warn(`invalid componentId: '${obj.#displayComponentId}'`); + } + } else { + obj.#currentComponentInfo = obj.#displayComponentId; + } + + return obj; + } + + /** + * Gets the spectrum's unique identifier. + * + * @returns The spectrum identifier. + */ + get id(): VisorSpectrumInfo['id'] { + return this.#spectrumInfo.id; + } + + /** + * Gets the underlying spectrum metadata. + * + * @returns The spectrum metadata associated with this instance. + */ + get metadata(): VisorSpectrumInfo { + return this.#spectrumInfo; + } + + /** + * Gets the component ID currently shared by the selected nodes. + * + * @returns The shared component ID, `null` when no component is selected, or + * `undefined` when the selected nodes use different components. + */ + get displayComponentId(): number | null | undefined { + return this.#displayComponentId; + } + + /** + * Gets all available spectrum components. + * + * The map is keyed by each component ID converted to a string. + * + * @returns A map of component IDs to component information. + */ + get componentOptions(): Map { + return this.#componentOptions; + } + + /** + * Gets information about the currently displayed component. + * + * @returns The current component information, `null` when no valid component + * is selected, or `undefined` when the selected nodes do not share a common + * component. + */ + get currentComponentInfo(): AggregateSpectrumComponentInfo | null | undefined { + return this.#currentComponentInfo; + } + + /** + * Changes the component displayed for the current spectrum. + * + * The spectrum must be the current spectrum in the associated selection. + * After the value is updated, the selection's spectrum-component-change + * callback is invoked when one is registered. + * + * @param id - The component ID to display. Numeric and string IDs are + * normalized to strings for lookup. Passing `null` or `undefined` clears or + * marks the component state accordingly. + * @returns Information for the selected component, `null` when the ID does + * not match an available component, or `undefined` when explicitly passed. + * @throws {Error} When there is no current spectrum. + * @throws {Error} When this instance is not the current spectrum. + */ + setDisplayComponentId = ( + id: number | string | null | undefined + ): AggregateSpectrumComponentInfo | null | undefined => { + if (this.#selectionInfo.currentSpectrumInfo == null) { + throw new Error('component should not be changed when the current spectrum is null'); + } else if (this.#selectionInfo.currentSpectrumInfo !== this) { + throw new Error('component should not be changed on a spectrum that is not current'); + } + + if (id != null) { + this.#currentComponentInfo = this.#componentOptions.get(id.toString()) ?? null; + this.#displayComponentId = this.#currentComponentInfo?.id ?? null; + } else { + this.#currentComponentInfo = id; + this.#displayComponentId = id; + } + + const events = this.#selectionInfo.events; + + if (events.onSpectrumComponentChange != null) { + events.onSpectrumComponentChange(this.#selectionInfo.currentSpectrumInfo); + } + + return this.#currentComponentInfo; + }; +} diff --git a/src/ansys/visor/visor-client/src/components/AwcIcons.js b/src/ansys/visor/visor-client/src/components/AwcIcons.js new file mode 100644 index 00000000..46c87b2b --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/AwcIcons.js @@ -0,0 +1,7886 @@ +const AwcIcons = (() => { + const buttonAttribute = (() => { + const min = 100000; + const max = 999999; + const r = Math.floor(Math.random() * (max - min + 1) + min); + return `data-icon-${r}`; + })(); + const buttonStyleElem = (() => { + const style = document.createElement('style'); + style.innerHTML = ` +/* +This style element was dynamically +generated by the AWC icon library. +*/ +button[${buttonAttribute}] { +background-color: transparent; +} +`; + return style; + })(); + const baseDiv = (() => { + const div = document.createElement('div'); + div.style.display = 'block'; + div.style.position = 'relative'; + div.style.overflow = 'hidden'; + div.style.userSelect = 'none'; + div.style.boxSizing = 'border-box'; + const scaleDiv = document.createElement('div'); + scaleDiv.style.display = 'block'; + scaleDiv.style.position = 'absolute'; + scaleDiv.style.width = '24px'; + scaleDiv.style.height = '24px'; + scaleDiv.style.transformOrigin = 'top left'; + scaleDiv.style.boxSizing = 'border-box'; + div.appendChild(scaleDiv); + return div; + })(); + + /** + * @param {string} str + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + function m(str, size, padding, makeButton, classNames) { + if (buttonStyleElem.parentNode !== document.head) { + document.head.prepend(buttonStyleElem); + } + const div = /**@type{HTMLDivElement}*/ baseDiv.cloneNode(true); + const scaleDiv = /**@type{HTMLDivElement}*/ div.children[0]; + size = Math.max(0, size ?? 24); + padding = Math.max(0, padding ?? 0); + scaleDiv.style.transform = `scale(${size / 24})`; + scaleDiv.style.inset = `${padding}px auto auto ${padding}px`; + div.style.padding = `${padding}px`; + div.style.width = `${size + padding * 2}px`; + div.style.height = `${size + padding * 2}px`; + scaleDiv.innerHTML = str; + /**@type{HTMLDivElement|HTMLButtonElement}*/ + let finalElem = div; + if (makeButton === true) { + const button = document.createElement('button'); + button.setAttribute(buttonAttribute, ''); + button.style.display = 'inline-block'; + button.style.margin = '0'; + button.style.padding = '0'; + button.style.borderWidth = '0'; + button.style.outlineWidth = '0'; + button.style.borderRadius = '5px'; + button.style.cursor = 'pointer'; + button.style.boxSizing = 'border-box'; + button.style.overflow = 'hidden'; + button.style.verticalAlign = 'top'; + button.style.color = 'inherit'; + button.appendChild(div); + finalElem = button; + } + classNames?.forEach((className) => { + finalElem.classList.add(className); + }); + return finalElem; + } + + return { + /** + * @param {number} [size] + * @return {HTMLDivElement} + */ + getNullIcon: (size) => { + const div = document.createElement('div'); + size = Math.max(0, size ?? 24); + div.style.width = `${size}px`; + div.style.height = `${size}px`; + return div; + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + get2dIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + get3dThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + get3dIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAccountIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAcousticIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAddCircleIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAddIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAdminIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAlbumIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAlignToNormalIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAmazonIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAmericanExpressIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + + + + + + + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAnalysisIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAngleIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAngularLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAnsysCreditsIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAnsysLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAppearanceIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getApplePayIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAppsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArchiveIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowBackIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowDoubleLeftIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowDoubleRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowDownIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowHeadDownIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowHeadLeftIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowHeadRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowHeadUpIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowNextIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowRightUpIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowSmallDownIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowSmallRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowSmallUpIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getArrowUpIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAssemblyThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAssemblyIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAssignedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAttachThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getAttachIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBankIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBodiesThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBodiesTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBodiesIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBookIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBrokenFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBugThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBugTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBugIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBuildingIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBulletThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getBullhornIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCadConfigureReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCadConfigureThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCadConfigureIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCalendarIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCanceledEventIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCardIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCenterIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getChangeOrganizationIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getChatThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getChatTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getChatIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCheckIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCheckboxCheckedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCheckboxIndeterminateIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCheckboxUncheckedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getChecklistIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getChipCodeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCircleThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCircleTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCircleIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCirrusIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCitiIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getClearAllIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCloseSmallIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCloseIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getClosedProjectIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCloudThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCloudTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCloudIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCodeThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCodeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getComfortableViewIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCompactViewIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCompareIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCompassIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getConeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getConfigurationIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getConnectorThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getConnectorIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getContactThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getContactTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getContactIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getContoursIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getConvectionCoefficientThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getConvectionCoefficientIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCoordSystemTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCoordSystemIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCoplanarIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCopyIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCoreHoursIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCreateFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCreateGroupIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCreateProjectIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCreateUserIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCriticalIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCubeRLeftIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCubeRRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCubeTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCubeTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCubeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCursorDefaultIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getCylinderIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDashboardIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDefaultViewIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDeformableIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDeformationIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDeleteIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDesignIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDesktopIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDiagramSecurityIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDiagramIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDiscordIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDiscoverIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDiscreteBodiesThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDiscreteBodiesIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDiscussionIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDisplacementThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDisplacementTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDisplacementIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDissatisfiedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDockBottomIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDockLeftIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDockRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDocumentReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDocumentThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDocumentIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDoorIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDownArrowIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDownSIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDownloadIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDropdownArrowIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getDuplicateIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEdgeOffIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEdgeOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEdgeThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEdgesOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEdgesIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEditEventIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEditModelIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEditIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getElectricalThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getElectricalTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getElectricalIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEmailIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEmojiIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEraseIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getErrorThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getErrorIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEventHistoryIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEventThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEventTimeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEventTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getEventIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getExclamationThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getExclamationIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getExpandIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getExperimentIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getExplosionIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getExportIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFaceOffIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFaceOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFaceThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFaceTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFacebookLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFacesEdgesOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFacesEdgesIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFacesOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFacesIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFastForwardIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFavoriteProjectIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFeedbackIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFileInputIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFileOutputIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFilterIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFlowInIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFlowOutletIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFlowThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFluidPressureThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFluidPressureIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFluidThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFluidTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFluidIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFolderSharedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFolderThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFolderTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFolderIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getForceAngleIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getForceThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getForceIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFullscreenExitIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFullscreenIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFunctionFxTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFunctionFxIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFunctionTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getFunctionIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGeometryEdgeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGeometryFaceIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGeometryPartIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGeometryVertexIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGhostIcon: (size, padding, makeButton, classNames) => { + const s = ``; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGiftIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGithubIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGlobalThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGlobalTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGlobalIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGlueIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGoogleLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGroupThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getGroupIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHamburgerIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHardeningCycleIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHeadingIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHeartIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHeatFluxTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHeatFluxIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHelpIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHideThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHideIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHomeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getHourglassIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIconIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getImportCadIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getImportIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInTextIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIndentDecreaseIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIndentIncreaseIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInfoOutlineIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInfoThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInfoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInletTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInletTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInletIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInstancesThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInstancesIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInteractionsThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getInteractionsIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIsolationThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIsolationIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIssueThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIssueTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getIssueIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getJupyterLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getKebabIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getKernelIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getKeyIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getKeyboardIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLabelIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLargeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLeftArrowIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLegendIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLegoThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLegoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLightbulbThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLightbulbTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLightbulbIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLineIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLinkIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLinkedinLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getListFxThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getListFxIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getListPlayIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getListThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getListTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getListIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLoadTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLoadTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLoadIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLockThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLockIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLogThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLogIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLogoutIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getLogsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMaestroIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMagicIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMassThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMassIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMastercardIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMateThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMateIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMaterialIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMaterialsThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMaterialsTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMaterialsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMeatballsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMechanicalThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMechanicalTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMechanicalIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMeshThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMeshIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMeshgroupTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMicFilledIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMicOutlineIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMicrosoftLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMicrosoftUniLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMinimizeThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMinimizeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMinusIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMobileSmartphoneIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getModelExportIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getModelImportIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getModelValidateIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getModelIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getModelerIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMoonIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getMoreIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNeutralIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNewChatIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNewTabIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNoResultsIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNormalVectorThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNormalVectorIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNoteThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNoteTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNoteIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNotificationAIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNotificationBellIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getNotificationIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getObject3dTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getObject3dTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getObject3dIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOnscaleIconLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOnshapeFileLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOnshapeSyncIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOpenFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOpenModelIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOpenProjectIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOrderIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOutFlowThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOutletTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOutletTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getOutletIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getParameterGroupThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getParameterThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getParameterIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPartOffIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPartOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPartThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPartWMaterialIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPastEventIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPasteIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPauseIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPaypalIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPdfFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPeopleIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPeriodicIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPersonIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPhoneIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPhysicsThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPhysicsTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPhysicsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPinsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPlayThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPlayTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPlayIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPlotsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPlusIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPowerThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPowerTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPowerIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPrIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPressureLoadIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPressureThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPressureTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getPressureIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getProjectSecurityIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getProjectThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getProjectTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getProjectIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getProtractorIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRadiationThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRadiationIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRadioActiveIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRadioCheckedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRadioIndeterminateIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRadioUncheckedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getReactLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRedoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRefreshIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRemoveIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRenameIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getReplayIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getReportsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getResetIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getResizeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRestartIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRestraintThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRestraintIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getResultsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRightArrowIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRightSIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRigidBodyThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRigidBodyIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRigidIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRocketIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRotateLeftIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRotateRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRotateSmallLeftIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRotateSmallRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRotationThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRotationTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRotationIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRulerThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRulerTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getRulerIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSageIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSatisfiedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSchoolIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getScissorsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSearchIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSecurityIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSelectThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSelectIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSemiFacesEdOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSemiFacesEdgesIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSemiFacesOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSemiFacesIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSendIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSensorThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSensorTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSensorIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSettingsOutlineIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSettingsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSharedProjectIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getShopifyIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getShowThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getShowIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSideCheckIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSideUncheckIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSignOutIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSimulatorIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSinglePlaneIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSkrillIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSlicingThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSlicingIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSmallIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSnapshotIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSortIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSpentIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSphereAltIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + + + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSphereIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSpinnerIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSplitHorizontallyIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSplitThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSplitUiIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSplitIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSpringThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSpringIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSquareLargeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSquareThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSquareTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSquareIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStarOutlineIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStarThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStarTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStarIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStickyIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStopIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStreamlinesIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStudyTreeRestingIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getStudyTreeSimulatingIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSubgroupThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSubgroupIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSuccessIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSunIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSupportIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSuppressIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSuspendIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSuspentIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSwapHorizontalIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSwapVerticalIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSweepIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSymmetryThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSymmetryTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getSymmetryIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTallIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTangentIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTargetIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTemperatureThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTemperatureTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTemperatureIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTextFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThemeIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThermalPowerIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThermalThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThermalTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThermalIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThumbsDownFilledIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThumbsDownIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThumbsUpFilledIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getThumbsUpIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTimeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTimeThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTimelineIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getToolsIcon: (size, padding, makeButton, classNames) => { + const s = ` +- + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getToroidIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTorqueLoadIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTorqueThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTorqueTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTorqueIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTractionLoadIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTractionTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTractionTreeIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTransactionsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTriangleThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTriangleTreeReversedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTriangleIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTurnLeftIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTurnRightIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getTwitterLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUiColumnIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUiLinkIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUnassignedIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUndoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUnionIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUnlinkIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUnlockThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUnlockIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUpSIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUpcomingEventIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUploadFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUploadIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUserSettingsIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getUserIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getValidFileIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getValidatorIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVersionLogThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVersionLogIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVersionIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVertexOffIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVertexOnIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVeryDissatisfiedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVerySatisfiedIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getView2Icon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getViewBlocksIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getViewListIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getViewIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVisaIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVisualizationActiveIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getVisualizationInactiveIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWarningThinIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWarningIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWatchIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWebBrowserIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWebComponentsLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWesternUnionIcon: (size, padding, makeButton, classNames) => { + const s = ` + + + + + + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWideIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWorkIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWorkflowIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getWrenchIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getYoutubeLogoIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getZoomInIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + /** + * @param {?number} [size] + * @param {?number} [padding] + * @param {?boolean} [makeButton] + * @param {?(string[])} [classNames] + * @return {HTMLDivElement|HTMLButtonElement} + */ + getZoomOutIcon: (size, padding, makeButton, classNames) => { + const s = ` + +`; + return m(s, size, padding, makeButton, classNames); + }, + }; +})(); + +export { AwcIcons }; diff --git a/src/ansys/visor/visor-client/src/components/FpsCounter.tsx b/src/ansys/visor/visor-client/src/components/FpsCounter.tsx new file mode 100644 index 00000000..23f33156 --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/FpsCounter.tsx @@ -0,0 +1,97 @@ +/** + * Displays a toggleable frames-per-second counter inside a specified container. + * + * The counter can be shown or hidden by pressing Ctrl+F10. + */ +export class FpsCounter { + /** + * Creates an FPS counter and attaches it to the provided container. + * + * @param container - The HTML element that will contain the FPS display. + * @param inset - A valid CSS `inset` value used to position the display. + */ + constructor(container: HTMLElement, inset: string) { + const fpsDisplay = document.createElement('div'); + fpsDisplay.style.position = 'absolute'; + fpsDisplay.style.inset = inset; + fpsDisplay.style.backgroundColor = 'rgba(0,0,0,0.75)'; + fpsDisplay.style.color = '#ffffff'; + fpsDisplay.style.padding = '10px'; + fpsDisplay.style.zIndex = '999'; + fpsDisplay.style.minWidth = '16ch'; + fpsDisplay.style.display = 'none'; + + fpsDisplay.innerHTML = ` + + + + + +
FPS:
+ `; + + this.#valueContainer = fpsDisplay.getElementsByTagName('td')[1]; + container.appendChild(fpsDisplay); + + let timeoutId: number | null = null; + + this.#fpsDisplay = fpsDisplay; + + this.#handler = (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === 'F10') { + const allow = timeoutId == null; + + if (allow) { + timeoutId = setTimeout(() => { + clearTimeout(timeoutId!); + timeoutId = null; + }, 250); + } else { + return; + } + + if (fpsDisplay.style.display === 'none') { + fpsDisplay.style.removeProperty('display'); + } else { + fpsDisplay.style.display = 'none'; + } + } + }; + + window.addEventListener('keydown', this.#handler); + } + + /** + * The element in which the current FPS value is displayed. + */ + readonly #valueContainer: HTMLElement; + + /** + * The root element of the FPS display. + */ + readonly #fpsDisplay: HTMLElement; + + /** + * Handles keyboard events used to toggle the FPS display. + * + * @param e - The keyboard event dispatched by the window. + */ + readonly #handler: (e: KeyboardEvent) => void; + + /** + * Updates the value shown by the FPS counter. + * + * @param val - The FPS value to display. + */ + setValue = (val: string | number): void => { + this.#valueContainer.innerHTML = val.toString(); + }; + + /** + * Removes the FPS display and unregisters its keyboard event listener. + */ + dispose = (): void => { + this.#fpsDisplay.remove(); + window.removeEventListener('keydown', this.#handler); + }; +} diff --git a/src/ansys/visor/visor-client/src/components/UiScaffold.tsx b/src/ansys/visor/visor-client/src/components/UiScaffold.tsx new file mode 100644 index 00000000..34586c4d --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/UiScaffold.tsx @@ -0,0 +1,420 @@ +import { FC, RefObject, useEffect, useRef, useState } from 'react'; +import { Panel_BottomMiddle, Panel_BottomMiddle_Util } from './ui-panels/Panel_BottomMiddle.tsx'; +import { Panel_BottomRight } from './ui-panels/Panel_BottomRight.tsx'; +import { Panel_BottomRight_Util } from './ui-panels/Panel_BottomRight_Util.tsx'; +import { Panel_TopRight } from './ui-panels/Panel_TopRight.tsx'; +import { Panel_TopLeft } from './ui-panels/Panel_TopLeft.tsx'; +import { ContextMenu, ContextMenuOptions, ContextMenuUtil } from './ui-panels/ContextMenu.tsx'; +import { VisorFrontend, VisorCameraState } from '../VisorFrontend.tsx'; +import { Panel_TopLeft_Util } from './ui-panels/Panel_TopLeft_Util.tsx'; +import { Panel_TopRight_Util } from './ui-panels/Panel_TopRight_Util.tsx'; +import { FpsCounter } from './FpsCounter.tsx'; +import { formatNumber, randomId, randomInt, roundFloat, setScale } from '../utils/JsHelpers'; + +export type UiScaffoldUtil = { + pixelDensity: number; + setUnit: (unit: string) => void; +}; + +export const UiScaffold: FC<{ + visorState: VisorFrontend; + onLoad: (util: UiScaffoldUtil) => void; +}> = ({ visorState, onLoad }) => { + const [contextMenuOptions, setContextMenuOptions] = useState(null!); + let contextMenuUtil: ContextMenuUtil = null!; + let panelTopLeftUtil: Panel_TopLeft_Util = null!; + let panelTopRightUtil: Panel_TopRight_Util = null!; + let panelBottomMiddleUtil: Panel_BottomMiddle_Util = null!; + let panelBottomRightUtil: Panel_BottomRight_Util = null!; + const showBottomLeft = false; + const showTopMiddle = false; + const rulerHeight = 10; + const rulerFontSize = 12; + const scaffoldElemId = randomId(); + const vtkCanvasContainerId = randomId(); + const rulerElemId = randomId(); + const rulerTextLeftElemId = randomId(); + const rulerTextRightElemId = randomId(); + const unitContainerElemId = randomId(); + const topLeftElemId = randomId(); + const topMiddleElemId = randomId(); + const topRightElemId = randomId(); + const bottomLeftElemId = randomId(); + const bottomRightElemId = randomId(); + const bottomMiddleElemId = randomId(); + + useEffect(() => { + const scaffoldElem = document.getElementById(scaffoldElemId)!; + const rulerElem = document.getElementById(rulerElemId)!; + const rulerTextLeftElem = document.getElementById(rulerTextLeftElemId)!; + const rulerTextRightElem = document.getElementById(rulerTextRightElemId)!; + const unitContainerElem = document.getElementById(unitContainerElemId)!; + const topLeftElem = document.getElementById(topLeftElemId)!; + const topMiddleElem = document.getElementById(topMiddleElemId)!; + const topRightElem = document.getElementById(topRightElemId)!; + const bottomLeftElem = document.getElementById(bottomLeftElemId)!; + const bottomRightElem = document.getElementById(bottomRightElemId)!; + const bottomMiddleElem = document.getElementById(bottomMiddleElemId)!; + const fpsCounterManager = new FpsCounter(scaffoldElem, 'auto 0 0 auto'); + visorState.addFrameRenderedListener((fps) => { + fpsCounterManager.setValue(formatNumber(fps, 4, true)); + }); + let pixelDensity: number = 1000; + let lastPixelDensity: number | null = null; + const setUiScale = () => { + if (pixelDensity <= 0 && lastPixelDensity != null && lastPixelDensity <= 0) { + return; + } + lastPixelDensity = pixelDensity; + const scale = pixelDensity > 0 ? scaffoldElem.offsetWidth / pixelDensity : 1; + // Apply after-scaling to some UI panels. + // see https://github.com/ansys-internal/theia/pull/1004 + const afterScale = 0.85; + setScale(topLeftElem, scale * afterScale); + setScale(topMiddleElem, scale); + setScale(topRightElem, scale * afterScale); + setScale(bottomLeftElem, scale); + setScale(bottomMiddleElem, scale); + setScale(bottomRightElem, scale * afterScale); + }; + setUiScale(); + + const vtkCanvasContainer = document.getElementById(vtkCanvasContainerId)!; + vtkCanvasContainer.appendChild(visorState.domElement); + void visorState.resizeAsync(); + + rulerElem.style.display = 'none'; + visorState.addCameraChangedListener((cameraState) => { + if (visorState.unit != null && visorState.unit !== '') { + updateRuler(cameraState); + } + }); + + function updateRuler(cameraState: VisorCameraState) { + if (cameraState.orthographic) { + rulerElem.style.removeProperty('display'); + const val = + cameraState.unitsPerPixel * + rulerElem.getBoundingClientRect().width * + window.devicePixelRatio; + rulerTextRightElem.innerHTML = roundFloat(val, 2).toString(); + return; + } + rulerElem.style.display = 'none'; + } + + const util: UiScaffoldUtil = { + set pixelDensity(value) { + pixelDensity = value; + setUiScale(); + }, + get pixelDensity() { + return pixelDensity; + }, + setUnit(unit) { + if (unit == null || unit === '') { + rulerElem.style.display = 'none'; + } else { + unit = unit.replace(//g, '>'); + unitContainerElem.innerHTML = unit; + visorState.getCameraStateAsync().then((cameraState) => { + updateRuler(cameraState); + }); + } + }, + }; + visorState.setUiScaffoldUtil(util); + onLoad(util); + const resizeObserver = new ResizeObserver(() => { + setUiScale(); + void visorState.resizeAsync(); + }); + resizeObserver.observe(scaffoldElem); + return () => { + resizeObserver.disconnect(); + fpsCounterManager.dispose(); + }; + }, []); + return ( +
+ (contextMenuUtil = u)} + options={contextMenuOptions} + /> +
+
+
+ (panelTopLeftUtil = u)} + > +
+
+
+ {showTopMiddle ? ( +
+ Top Middle +
+ ) : null} +
+
+
+ (panelTopRightUtil = u)} + > +
+
+
+
+ (panelBottomRightUtil = u)} + > +
+
+
+
+ (panelBottomMiddleUtil = u)} + > +
+
+
+ Scale +
+
+
+
+
+
+ {visorState.unit} +
+
+
+
+
+
+ {showBottomLeft ? ( +
+ Bottom Left +
+ ) : null} +
+
+ ); +}; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/ContextMenu.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/ContextMenu.tsx new file mode 100644 index 00000000..304e5d47 --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/ContextMenu.tsx @@ -0,0 +1,63 @@ +import { FC, useEffect } from 'react'; +import { VisorFrontend } from '../../VisorFrontend.tsx'; +import { VisorSceneNodeExtended } from '../../state/VisorSceneGraph.tsx'; +import { randomId } from '../../utils/JsHelpers'; + +export type ContextMenuUtil = {}; + +export type ContextMenuOptions = { + node: VisorSceneNodeExtended | null; + x: number; + y: number; +}; + +export const ContextMenu: FC<{ + visorState: VisorFrontend; + onLoad: (util: ContextMenuUtil) => void; + options?: ContextMenuOptions | null; +}> = ({ onLoad, options }) => { + const contextMenuElemId = randomId(); + const { node, x, y } = options ?? { + node: null, + x: 0, + y: 0, + }; + useEffect(() => { + const contextMenuElem = document.getElementById(contextMenuElemId) as HTMLDivElement; + onLoad({}); + }, []); + + return ( +
+ {node != null && ( + + + + + + + + + + + + + + + +
ID:{node.id}
Type:{node.nodeType}
Name:{node.name}
+ )} +
+ ); +}; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx new file mode 100644 index 00000000..30e873fb --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx @@ -0,0 +1,213 @@ +import { FC, RefObject, useEffect, useRef } from 'react'; +import { AwcIcons } from '../AwcIcons'; +import { VisorFrontend } from '../../VisorFrontend.tsx'; +import { makeTooltip } from '../../utils/Tooltip'; + +export type Panel_BottomMiddle_Util = {}; + +export const Panel_BottomMiddle: FC<{ + visorState: VisorFrontend; + onLoad: (util: Panel_BottomMiddle_Util) => void; +}> = ({ visorState, onLoad }) => { + const edgeVisibility: RefObject = useRef(false); + const tableElemId = `tableElem-${crypto.randomUUID()}`; + const selectionModeContainerId = `selMode-${crypto.randomUUID()}`; + useEffect(() => { + const perspectiveOrthographicButton = AwcIcons.getWatchIcon(null, 5, true, [ + 'theme-hover-background-3', + ]); + const crossSectionButton = AwcIcons.getSinglePlaneIcon(null, 5, true, [ + 'theme-hover-background-3', + ]); + const wireFrameButton = AwcIcons.getMeshIcon(null, 5, true, ['theme-hover-background-3']); + const getFullScreenButton = AwcIcons.getExpandIcon(null, 5, true, [ + 'theme-hover-background-3', + ]); + const boundingBoxButton = AwcIcons.getModelerIcon(null, 5, true, [ + 'theme-hover-background-3', + ]); + const tableElem = document.getElementById(tableElemId) as HTMLTableElement; + const cells = tableElem.rows[0].cells; + ////////////////////////// + // get references to elements + const perspectiveOrthographicCell = cells[0]; + const crossSectionCell = cells[1]; + const wireFrameCell = cells[2]; + const getFullScreenCell = cells[3]; + const boundingBoxCell = cells[4]; + ////////////////////////// + // append buttons to elements + perspectiveOrthographicCell.appendChild(perspectiveOrthographicButton); + crossSectionCell.appendChild(crossSectionButton); + wireFrameCell.appendChild(wireFrameButton); + getFullScreenCell.appendChild(getFullScreenButton); + boundingBoxCell.appendChild(boundingBoxButton); + ////////////////////////// + // set tooltips + const cleanupFuncs = [ + makeTooltip(perspectiveOrthographicButton, 'Perspective / Orthographic'), + makeTooltip(crossSectionButton, 'Cross-section'), + makeTooltip(wireFrameButton, 'Edges / Wireframe'), + makeTooltip(getFullScreenButton, 'Fullscreen'), + makeTooltip(boundingBoxButton, 'Bounding Box'), + ]; + ////////////////////////// + // set onclick events on buttons + perspectiveOrthographicCell.onclick = async () => { + await visorState.setOrthographicModeAsync(); + await visorState.render(); + }; + crossSectionCell.onclick = async () => { + await visorState.setCrossSectionVisibilityAsync(); + await visorState.render(); + }; + wireFrameCell.onclick = async () => { + await visorState.setEdgeVisibilityAsync(); + await visorState.render(); + }; + getFullScreenCell.onclick = async () => { + await visorState.toggleFullScreenAsync(); + }; + boundingBoxCell.onclick = async () => { + await visorState.setBoundingBoxVisibilityAsync(); + await visorState.render(); + }; + + ////////////////////////// + // Selection mode widget + const selModeContainer = document.getElementById( + selectionModeContainerId + ) as HTMLDivElement; + type SelectionMode = 'part' | 'vertex' | 'edge' | 'face'; + + const modeIconFactories: Record HTMLElement> = { + part: () => AwcIcons.getGeometryPartIcon(null, 5, true, ['theme-hover-background-3']), + edge: () => AwcIcons.getGeometryEdgeIcon(null, 5, true, ['theme-hover-background-3']), + face: () => AwcIcons.getGeometryFaceIcon(null, 5, true, ['theme-hover-background-3']), + vertex: () => + AwcIcons.getGeometryVertexIcon(null, 5, true, ['theme-hover-background-3']), + }; + const modeLabels: Record = { + part: 'Select Part', + edge: 'Select Edge', + face: 'Select Face', + vertex: 'Select Vertex', + }; + + // The currently displayed icon button + let currentModeButton: HTMLElement = modeIconFactories.part(); + makeTooltip(currentModeButton, modeLabels.part); + selModeContainer.appendChild(currentModeButton); + + // Floating popup โ€“ appended to so it escapes overflow:auto containers + const popup = document.createElement('div'); + popup.className = 'theme-panel-1'; + popup.style.cssText = ` + position: fixed; + display: none; + flex-direction: row; + gap: 2px; + padding: 6px; + border-radius: 4px; + z-index: 1000; + white-space: nowrap; + `; + document.body.appendChild(popup); + + const subModes: SelectionMode[] = ['edge', 'face', 'vertex']; + const popupCleanupFuncs: Array<() => void> = []; + for (const mode of subModes) { + const btn = modeIconFactories[mode](); + popupCleanupFuncs.push(makeTooltip(btn, modeLabels[mode])); + btn.onclick = (e) => { + e.stopPropagation(); + popup.style.display = 'none'; + selectMode(mode); + }; + popup.appendChild(btn); + } + + let popupOpen = false; + + function selectMode(mode: SelectionMode) { + // Reset current-mode button + currentModeButton.remove(); + currentModeButton = modeIconFactories[mode](); + makeTooltip(currentModeButton, modeLabels[mode]); + selModeContainer.appendChild(currentModeButton); + popupOpen = false; + popup.style.display = 'none'; + + currentModeButton.onclick = (e) => { + e.stopPropagation(); + if (mode === 'part') { + togglePopup(); + } else { + // clicking again resets to part + selectMode('part'); + } + }; + + visorState.setSelectionMode(mode); + } + + function togglePopup() { + popupOpen = !popupOpen; + if (popupOpen) { + popup.style.display = 'flex'; + // Position above the current mode button + const rect = currentModeButton.getBoundingClientRect(); + popup.style.left = `${rect.left + rect.width / 2}px`; + popup.style.top = `${rect.top}px`; + popup.style.transform = 'translateX(-50%) translateY(-100%) translateY(-4px)'; + } else { + popup.style.display = 'none'; + } + } + + currentModeButton.onclick = (e) => { + e.stopPropagation(); + togglePopup(); + }; + + const closePopupOnOutsideClick = (e: MouseEvent) => { + if (!selModeContainer.contains(e.target as Node)) { + popup.style.display = 'none'; + popupOpen = false; + } + }; + document.addEventListener('click', closePopupOnOutsideClick); + + cleanupFuncs.push(...popupCleanupFuncs); + cleanupFuncs.push(() => document.removeEventListener('click', closePopupOnOutsideClick)); + cleanupFuncs.push(() => popup.remove()); + ////////////////////////// + onLoad({}); + ////////////////////////// + return () => { + for (const func of cleanupFuncs) { + func(); + } + }; + }, []); + + return ( +
+ + + + + + + + + + +
+
+
+ ); +}; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomRight.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomRight.tsx new file mode 100644 index 00000000..c393de9f --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomRight.tsx @@ -0,0 +1,162 @@ +import { FC, useEffect } from 'react'; +import { VisorFrontend } from '../../VisorFrontend.tsx'; +import { Panel_BottomRight_Util } from './Panel_BottomRight_Util.tsx'; +import { formatNumber, randomId } from '../../utils/JsHelpers'; + +const FMT_PLACES = 6; +const FMT_PAD = false; + +function fmtCoord(v: number): string { + return formatNumber(v, FMT_PLACES, FMT_PAD); +} + +export const Panel_BottomRight: FC<{ + visorState: VisorFrontend; + onLoad: (util: Panel_BottomRight_Util) => void; +}> = ({ visorState, onLoad }) => { + const containerId = randomId(); + const contentId = randomId(); + const copyButtonId = randomId(); + + useEffect(() => { + const container = document.getElementById(containerId) as HTMLDivElement; + const content = document.getElementById(contentId) as HTMLDivElement; + const copyButton = document.getElementById(copyButtonId) as HTMLButtonElement; + + // Initially hidden + container.style.display = 'none'; + + let currentResult: any = null; + + function buildContent(result: any): { html: string; text: string } { + if (!result || !result.found) { + return { html: '', text: '' }; + } + if (result.mode === 'vertex') { + const [x, y, z] = result.position as number[]; + const text = `X: ${fmtCoord(x)} Y: ${fmtCoord(y)} Z: ${fmtCoord(z)}`; + const html = ` + + + +
X${fmtCoord(x)}
Y${fmtCoord(y)}
Z${fmtCoord(z)}
`; + return { html, text }; + } else if (result.mode === 'edge') { + const [ax, ay, az] = result.pointA as number[]; + const [bx, by, bz] = result.pointB as number[]; + const len: number = result.length; + const text = + `Point A: (${fmtCoord(ax)}, ${fmtCoord(ay)}, ${fmtCoord(az)}) ` + + `Point B: (${fmtCoord(bx)}, ${fmtCoord(by)}, ${fmtCoord(bz)}) ` + + `Length: ${fmtCoord(len)}`; + const html = ` + + + + + + + + + + + + + + + + + + + + +
Point A
X${fmtCoord(ax)}Y${fmtCoord(ay)}
Z${fmtCoord(az)}
Point B
X${fmtCoord(bx)}Y${fmtCoord(by)}
Z${fmtCoord(bz)}
Length${fmtCoord(len)}
`; + return { html, text }; + } else if (result.mode === 'face') { + const area: number = result.area; + const text = `Area: ${fmtCoord(area)}`; + const html = ` + +
Area${fmtCoord(area)}
`; + return { html, text }; + } + return { html: '', text: '' }; + } + + function showResult(result: any) { + currentResult = result; + if (!result || !result.found) { + container.style.display = 'none'; + return; + } + const { html, text } = buildContent(result); + if (!html) { + container.style.display = 'none'; + return; + } + content.innerHTML = html; + copyButton.onclick = () => { + navigator.clipboard.writeText(text).catch(() => { + // fallback: do nothing if clipboard API not available + }); + }; + container.style.display = 'block'; + } + + function clearResult() { + currentResult = null; + container.style.display = 'none'; + content.innerHTML = ''; + } + + // Listen for geometry pick results + const removeGeometryListener = visorState.addGeometryPickedListener(showResult); + + // Hide panel when switching back to 'part' mode; reset on any mode change + const removeModeListener = visorState.addSelectionModeChangedListener((mode) => { + clearResult(); + if (mode === 'part') { + container.style.display = 'none'; + } + }); + + const util = new Panel_BottomRight_Util(showResult, clearResult); + onLoad(util); + + return () => { + removeGeometryListener(); + removeModeListener(); + }; + }, []); + + return ( +
+
+
+ +
+
+ ); +}; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomRight_Util.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomRight_Util.tsx new file mode 100644 index 00000000..ebf78f14 --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomRight_Util.tsx @@ -0,0 +1,9 @@ +export class Panel_BottomRight_Util { + constructor(setPickResult: (result: any) => void, clearPickResult: () => void) { + this.setPickResult = setPickResult; + this.clearPickResult = clearPickResult; + } + + readonly setPickResult: (result: any) => void; + readonly clearPickResult: () => void; +} diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx new file mode 100644 index 00000000..5f3bf8a8 --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx @@ -0,0 +1,120 @@ +import { FC, RefObject, useEffect, useRef } from 'react'; +import { TreeView, TreeViewUtil } from '../../treeview/TreeView.tsx'; +import { VisorFrontend } from '../../VisorFrontend.tsx'; +import { VisorSceneNodeExtended } from '../../state/VisorSceneGraph.tsx'; +import { AwcIcons } from '../AwcIcons'; +import { Panel_TopLeft_Util } from './Panel_TopLeft_Util.tsx'; +import { getRootFontSize, randomId } from '../../utils/JsHelpers'; + +export const Panel_TopLeft: FC<{ + visorState: VisorFrontend; + onLoad: (util: Panel_TopLeft_Util) => void; +}> = ({ visorState, onLoad }) => { + const treeViewUtil: RefObject> = useRef(null!); + const reactComponentContainerId = randomId(); + const collapseButtonContainerId = randomId(); + const treeViewContainerId = randomId(); + useEffect(() => { + const reactComponentContainer = document.getElementById( + reactComponentContainerId + ) as HTMLDivElement; + const collapseButtonContainer = document.getElementById( + collapseButtonContainerId + ) as HTMLTableCellElement; + const treeViewContainer = document.getElementById(treeViewContainerId) as HTMLDivElement; + treeViewUtil.current.addSelectionChangeListener((selectedNodes) => { + (async () => { + for (const node of visorState.sceneGraph.descendantActorNodesOrSelfArray) { + await node.setSelectedAsync(false); + } + for (const node of selectedNodes) { + await node.setSelectedAsync(true); + } + await visorState.render(); + })(); + }); + treeViewUtil.current.addVisibilityChangeListener((visibleNodes) => { + (async () => { + for (const node of visorState.sceneGraph.descendantActorNodesOrSelfArray) { + await node.setVisibilityAsync(false); + } + for (const node of visibleNodes) { + await node.setVisibilityAsync(true); + } + // await visorState.updateBoundingBoxBoundsAsync(); + await visorState.render(); + })(); + }); + const fontSize: number = getRootFontSize(14); + const collapseButton = AwcIcons.getArrowHeadDownIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + const expandButton = AwcIcons.getArrowHeadUpIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + let isPanelCollapsed = false; + collapseButton.onclick = (e) => { + reactComponentContainer.style.width = `${reactComponentContainer.offsetWidth}px`; + treeViewContainer.style.display = 'none'; + collapseButton.remove(); + collapseButtonContainer.appendChild(expandButton); + isPanelCollapsed = true; + }; + expandButton.onclick = (e) => { + reactComponentContainer.style.removeProperty('width'); + treeViewContainer.style.removeProperty('display'); + expandButton.remove(); + collapseButtonContainer.appendChild(collapseButton); + isPanelCollapsed = false; + }; + expandButton.click(); + visorState.setTreeViewUtil(treeViewUtil.current); + const util = new Panel_TopLeft_Util( + treeViewUtil.current, + () => { + expandButton.click(); + }, + () => { + collapseButton.click(); + }, + () => { + return isPanelCollapsed; + } + ); + visorState.setPanelTopLeftUtil(util); + onLoad(util); + }, []); + return ( +
+ + + + + + + +
Objects
+
+ { + treeViewUtil.current = u; + // treeViewReady.current(); + }} + /> +
+
+ ); +}; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft_Util.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft_Util.tsx new file mode 100644 index 00000000..86248969 --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft_Util.tsx @@ -0,0 +1,25 @@ +import { TreeViewUtil } from '../../treeview/TreeView.tsx'; +import { VisorSceneNodeExtended } from '../../state/VisorSceneGraph.tsx'; + +export class Panel_TopLeft_Util { + constructor( + treeViewUtil: TreeViewUtil, + expandPanel: () => void, + collapsePanel: () => void, + getIsPanelCollapsed: () => boolean + ) { + this.treeViewUtil = treeViewUtil; + this.expandPanel = expandPanel; + this.collapsePanel = collapsePanel; + this.#getIsPanelCollapsed = getIsPanelCollapsed; + } + + readonly treeViewUtil: TreeViewUtil; + readonly expandPanel: () => void; + readonly collapsePanel: () => void; + readonly #getIsPanelCollapsed: () => boolean; + + get isPanelCollapsed() { + return this.#getIsPanelCollapsed(); + } +} diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx new file mode 100644 index 00000000..631d4647 --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx @@ -0,0 +1,1336 @@ +import { FC, useEffect } from 'react'; +import { VisorFrontend } from '../../VisorFrontend.tsx'; +import { AwcIcons } from '../AwcIcons'; +import { VisorSceneNodeExtended } from '../../state/VisorSceneGraph.tsx'; +import { AggregateSpectrumComponentInfo } from '../../aggregate/AggregateSpectrumComponentInfo.tsx'; +import { AggregateSpectrumInfo } from '../../aggregate/AggregateSpectrumInfo.tsx'; +import { Panel_TopRight_Util } from './Panel_TopRight_Util.tsx'; +import { AggregateSelectionInfo } from '../../aggregate/AggregateSelectionInfo.tsx'; +import { + escapeHtml, + formatNumber, + getRootFontSize, + isNumeric, + randomId, +} from '../../utils/JsHelpers'; + +export const Panel_TopRight: FC<{ + visorState: VisorFrontend; + onLoad: (util: Panel_TopRight_Util) => void; +}> = ({ visorState, onLoad }) => { + // Opacity is stored internally and in appState as a normalized value in [0, 1]. + // The UI (slider/text) is shown as percent in [0, 100]. + const opacity01ToPercent = (opacity01: number): number => { + if (!isFinite(opacity01)) { + return 100; + } + const clamped = Math.min(1, Math.max(0, opacity01)); + return Math.round(clamped * 100); + }; + const opacityPercentTo01 = (opacityPct: number): number => { + if (!isFinite(opacityPct)) { + return 1; + } + const clamped = Math.min(100, Math.max(0, opacityPct)); + return clamped / 100; + }; + + const componentContainerId = randomId(); + const collapseButtonContainerId = randomId(); + const collapsiblePanelId = randomId(); + const legendCollapseButtonContainerId = randomId(); + const legendCollapsibleElemId = randomId(); + const paintIconClass = randomId(); + const nameInputElemId = randomId(); + const nameDifferentElemId = randomId(); + const opacityTextInputTableId = randomId(); + const opacityDifferentElemId = randomId(); + const opacityRangeInputElemId = randomId(); + const opacityTextInputElemId = randomId(); + const opacityResetButtonId = randomId(); + const legendPanelNoRangeElemId = randomId(); + const variableSelectElemId = randomId(); + const legendPanelRangeContainerId = randomId(); + const propertyTabIconContainerId = randomId(); + const legendTabIconContainerId = randomId(); + const propertyTabTextContainerId = randomId(); + const legendTabTextContainerId = randomId(); + const propertyTabElemId = randomId(); + const legendTabElemId = randomId(); + const propertyPanelElemId = randomId(); + const propertyPanelNoPartsElemId = randomId(); + const propertyPanelBodyElemId = randomId(); + const legendPanelBodyElemId = randomId(); + const legendPanelElemId = randomId(); + const legendOverlayElemId = randomId(); + const legendOverlayTitleContainerId = randomId(); + const legendGradientElemId = randomId(); + const legendOverlayElemId_alt = randomId(); + const legendOverlayTitleContainerId_alt = randomId(); + const legendGradientElemId_alt = randomId(); + const legendGradientMinTextContainerId = randomId(); + const legendGradientMidTextContainerId = randomId(); + const legendGradientMaxTextContainerId = randomId(); + const legendGradientMinTextContainerId_alt = randomId(); + const legendGradientMidTextContainerId_alt = randomId(); + const legendGradientMaxTextContainerId_alt = randomId(); + const legendPanelMinInputId = randomId(); + const legendPanelMaxInputId = randomId(); + const legendPanelMinResetButtonId = randomId(); + const legendPanelMaxResetButtonId = randomId(); + const constantRgbInputContainerId = randomId(); + const diffuseColorResetButtonId = randomId(); + const diffuseColorInputId = randomId(); + const diffuseColorDifferentElemId = randomId(); + const propertyPanelComponentContainerId = randomId(); + const componentSelectElemId = randomId(); + const legendPanelNoPartsElemId = randomId(); + const propertyPanelNoComponentContainerId = randomId(); + const legendPanelApplyRangeButtonId = randomId(); + useEffect(() => { + const componentContainer = document.getElementById(componentContainerId) as HTMLDivElement; + const collapseButtonContainer = document.getElementById( + collapseButtonContainerId + ) as HTMLTableCellElement; + const collapsiblePanel = document.getElementById(collapsiblePanelId) as HTMLDivElement; + const legendCollapseButtonContainer = document.getElementById( + legendCollapseButtonContainerId + ) as HTMLTableCellElement; + const legendCollapsibleElem = document.getElementById( + legendCollapsibleElemId + ) as HTMLDivElement; + const nameInputElem = document.getElementById(nameInputElemId) as HTMLInputElement; + const nameDifferentElem = document.getElementById(nameDifferentElemId) as HTMLDivElement; + const opacityTextInputTable = document.getElementById( + opacityTextInputTableId + ) as HTMLElement; + const opacityDifferentElem = document.getElementById( + opacityDifferentElemId + ) as HTMLDivElement; + const opacityRangeInputElem = document.getElementById( + opacityRangeInputElemId + ) as HTMLInputElement; + const opacityTextInputElem = document.getElementById( + opacityTextInputElemId + ) as HTMLInputElement; + const opacityResetButton = document.getElementById(opacityResetButtonId) as HTMLElement; + const legendPanelNoRangeElem = document.getElementById( + legendPanelNoRangeElemId + ) as HTMLDivElement; + const variableSelectElem = document.getElementById( + variableSelectElemId + ) as HTMLSelectElement; + const legendPanelRangeContainer = document.getElementById( + legendPanelRangeContainerId + ) as HTMLDivElement; + const propertyTabIconContainer = document.getElementById( + propertyTabIconContainerId + ) as HTMLDivElement; + const legendTabIconContainer = document.getElementById( + legendTabIconContainerId + ) as HTMLDivElement; + const propertyTabTextContainer = document.getElementById( + propertyTabTextContainerId + ) as HTMLDivElement; + const legendTabTextContainer = document.getElementById( + legendTabTextContainerId + ) as HTMLDivElement; + const propertyTabElem = document.getElementById(propertyTabElemId) as HTMLDivElement; + const legendTabElem = document.getElementById(legendTabElemId) as HTMLDivElement; + const propertyPanelElem = document.getElementById(propertyPanelElemId) as HTMLDivElement; + const legendPanelElem = document.getElementById(legendPanelElemId) as HTMLDivElement; + const propertyPanelNoPartsElem = document.getElementById( + propertyPanelNoPartsElemId + ) as HTMLDivElement; + const propertyPanelBodyElem = document.getElementById( + propertyPanelBodyElemId + ) as HTMLDivElement; + const legendPanelBodyElem = document.getElementById( + legendPanelBodyElemId + ) as HTMLDivElement; + const legendOverlayElem = document.getElementById(legendOverlayElemId) as HTMLDivElement; + const legendOverlayTitleContainer = document.getElementById( + legendOverlayTitleContainerId + ) as HTMLDivElement; + const legendGradientElem = document.getElementById(legendGradientElemId) as HTMLDivElement; + const legendOverlayElem_alt = document.getElementById( + legendOverlayElemId_alt + ) as HTMLDivElement; + const legendOverlayTitleContainer_alt = document.getElementById( + legendOverlayTitleContainerId_alt + ) as HTMLDivElement; + const legendGradientElem_alt = document.getElementById( + legendGradientElemId_alt + ) as HTMLDivElement; + const legendGradientMinTextContainer = document.getElementById( + legendGradientMinTextContainerId + ) as HTMLDivElement; + const legendGradientMidTextContainer = document.getElementById( + legendGradientMidTextContainerId + ) as HTMLDivElement; + const legendGradientMaxTextContainer = document.getElementById( + legendGradientMaxTextContainerId + ) as HTMLDivElement; + const legendGradientMinTextContainer_alt = document.getElementById( + legendGradientMinTextContainerId_alt + ) as HTMLDivElement; + const legendGradientMidTextContainer_alt = document.getElementById( + legendGradientMidTextContainerId_alt + ) as HTMLDivElement; + const legendGradientMaxTextContainer_alt = document.getElementById( + legendGradientMaxTextContainerId_alt + ) as HTMLDivElement; + const legendPanelMinInput = document.getElementById( + legendPanelMinInputId + ) as HTMLInputElement; + const legendPanelMaxInput = document.getElementById( + legendPanelMaxInputId + ) as HTMLInputElement; + const legendPanelMinResetButton = document.getElementById( + legendPanelMinResetButtonId + ) as HTMLAnchorElement; + const legendPanelMaxResetButton = document.getElementById( + legendPanelMaxResetButtonId + ) as HTMLAnchorElement; + const constantRgbInputContainer = document.getElementById( + constantRgbInputContainerId + ) as HTMLDivElement; + const diffuseColorResetButton = document.getElementById( + diffuseColorResetButtonId + ) as HTMLAnchorElement; + const propertyPanelComponentContainer = document.getElementById( + propertyPanelComponentContainerId + ) as HTMLDivElement; + const componentSelectElem = document.getElementById( + componentSelectElemId + ) as HTMLSelectElement; + const diffuseColorInput = document.getElementById(diffuseColorInputId) as HTMLInputElement; + const diffuseColorDifferentElem = document.getElementById( + diffuseColorDifferentElemId + ) as HTMLDivElement; + const legendPanelNoPartsElem = document.getElementById( + legendPanelNoPartsElemId + ) as HTMLDivElement; + const propertyPanelNoComponentContainer = document.getElementById( + propertyPanelNoComponentContainerId + ) as HTMLDivElement; + const legendPanelApplyRangeButton = document.getElementById( + legendPanelApplyRangeButtonId + ) as HTMLButtonElement; + + const fontSize: number = getRootFontSize(14); + + let collapsePanel: () => void; + let expandPanel: () => void; + let getIsPanelCollapsed: () => boolean; + let collapseLegend: () => void; + let expandLegend: () => void; + let getIsLegendCollapsed: () => boolean; + + { + // set up top right panel collapse/expand buttons + const collapseButton = AwcIcons.getArrowHeadDownIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + const expandButton = AwcIcons.getArrowHeadUpIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + const componentMinHeight: string = componentContainer.style.minHeight; + let isCollapsed = false; + collapseButton.onclick = (e) => { + componentContainer.style.width = `${componentContainer.offsetWidth}px`; + componentContainer.style.removeProperty('min-height'); + collapsiblePanel.style.display = 'none'; + collapseButton.remove(); + collapseButtonContainer.appendChild(expandButton); + isCollapsed = true; + }; + expandButton.onclick = (e) => { + componentContainer.style.removeProperty('width'); + componentContainer.style.minHeight = componentMinHeight; + collapsiblePanel.style.removeProperty('display'); + expandButton.remove(); + collapseButtonContainer.appendChild(collapseButton); + isCollapsed = false; + }; + expandButton.click(); + collapsePanel = () => { + collapseButton.click(); + }; + expandPanel = () => { + expandButton.click(); + }; + getIsPanelCollapsed = () => { + return isCollapsed; + }; + } + { + // set up legend overlay collapse/expand buttons + const collapseButton = AwcIcons.getArrowHeadDownIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + const expandButton = AwcIcons.getArrowHeadUpIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + let isCollapsed = false; + collapseButton.onclick = (e) => { + legendOverlayElem.style.width = `${legendOverlayElem.offsetWidth}px`; + legendCollapsibleElem.style.display = 'none'; + collapseButton.remove(); + legendCollapseButtonContainer.appendChild(expandButton); + isCollapsed = true; + }; + expandButton.onclick = (e) => { + legendOverlayElem.style.removeProperty('width'); + legendCollapsibleElem.style.removeProperty('display'); + expandButton.remove(); + legendCollapseButtonContainer.appendChild(collapseButton); + isCollapsed = false; + }; + expandButton.click(); + collapseLegend = () => { + collapseButton.click(); + }; + expandLegend = () => { + expandButton.click(); + }; + getIsLegendCollapsed = () => { + return isCollapsed; + }; + } + + const propertyTabIcon = AwcIcons.getEdgeOnIcon(24, 10, true); + const legendTabIcon = AwcIcons.getLegendIcon(24, 10, true); + propertyTabIconContainer.appendChild(propertyTabIcon); + legendTabIconContainer.appendChild(legendTabIcon); + let tabIndex = 0; + + propertyTabElem.onclick = () => { + propertyTabElem.classList.add('theme-background-1'); + propertyPanelElem.style.removeProperty('display'); + propertyTabTextContainer.style.removeProperty('display'); + legendTabElem.classList.remove('theme-background-1'); + legendPanelElem.style.display = 'none'; + legendTabTextContainer.style.display = 'none'; + tabIndex = 0; + }; + legendTabElem.onclick = () => { + propertyTabElem.classList.remove('theme-background-1'); + propertyPanelElem.style.display = 'none'; + propertyTabTextContainer.style.display = 'none'; + legendTabElem.classList.add('theme-background-1'); + legendPanelElem.style.removeProperty('display'); + legendTabTextContainer.style.removeProperty('display'); + tabIndex = 1; + }; + propertyTabElem.onclick(null!); + + function fixValue(val: number): string { + if (val >= 100000 || val <= -100000) { + return val.toExponential(5); + } + return formatNumber(val, 5, true); + } + + function updateMinLabel(min?: string | number | null | undefined, typing?: boolean): void { + let minStr: string = ''; + let placeholderMin: string = ''; + if (typeof min === 'number') { + minStr = min.toString(); + } else if (typeof min === 'string') { + minStr = min; + } else if (min === null) { + placeholderMin = '(not set)'; + } else if (min === undefined) { + placeholderMin = '(different)'; + } + legendPanelMinInput.placeholder = placeholderMin; + if (typing !== true && legendPanelMinInput.value != minStr) { + legendPanelMinInput.value = minStr; + } + } + + function updateMaxLabel(max?: string | number | null | undefined, typing?: boolean): void { + let maxStr: string = ''; + let placeholderMax: string = ''; + if (typeof max === 'number') { + maxStr = max.toString(); + } else if (typeof max === 'string') { + maxStr = max; + } else if (max === null) { + placeholderMax = '(not set)'; + } else if (max === undefined) { + placeholderMax = '(different)'; + } + legendPanelMaxInput.placeholder = placeholderMax; + if (typing !== true && legendPanelMaxInput.value != maxStr) { + legendPanelMaxInput.value = maxStr; + } + } + + const verticalBackgroundImageString = (() => { + // base64 for the color gradient + let s: string = ''; + s += + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAKNCAIAAABumpZUAAABEElEQVR4nK1RQRLDIAhcrNf'; + s += + '+/2299xmJ1k40RSKKpgdmEZZdVP96wpM7AiTwW6e6XmpVv8yUPtOQtWpemeH+FUfbzwkvqr0u/vxe0sspXuyegIIyp0F9'; + s += + 'ND/SXeH2eC3OrL+mYdVc9bdytT+c9XIT72zdafL/Yj6HhHtMmPINR75n3HIeMpbzLnjWmtQ/scc3+m9SS9NtzWevwPV69'; + s += + '2q8TYDzEZTikd43RUGec4RnucLjfYgzecZztR5l/bTTr8b4aPE7/tWsdp8GH9puiv7ZJ49YAkdAoKyv1DTdEfeu1z/mtf'; + s += + 'cYadzZdTV6WrIXDO8iOWC1mf3Dwl8F1DuMsHW3Hq+lo3G0vsWrp2+ZsXAvs+8PuGpfK7VoY4AAAAAASUVORK5CYII='; + return `url('${s}')`; + })(); + + const horizontalBackgroundImageString = (() => { + // base64 for the color gradient + let s: string = ''; + s += + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAo0AAAABCAIAAAA0IgOQAAABBElEQVR4nJ2TWxLDIAhFLya'; + s += + '/3f/a+t9lJNioVVGUmHYYhsfliHZC7xfIBQMJf1WoVlJa60mW6nlEplWvZIVfW/pcJ4BUgQ2/bCiBTgHztuHXeRnQpDLT'; + s += + 'zwiLgmEXJnCI1SP2+DrfFugnXQG6u9ewj1t4Tx9jJpweDBwIwRn9EQOOPsWn6Nrp0bX8RGPyDzmoCej5l57L7HDD9lIM5'; + s += + '0EeG2hD8iUoHnsOVLfUIWLac9fVWYqc8KW1GnSaEb+K9W6tBvpQxfnV4x8fDMEgvFfxbaoJM8FT4B96fZfZyKMF1m04KI'; + s += 's8v5FsIae3+/DaA7I4Yua7JYfdbkq3dN0ADjmGzBA04s8X9epU+Fr1e90AAAAASUVORK5CYII='; + return `url('${s}')`; + })(); + + legendGradientElem.style.backgroundImage = verticalBackgroundImageString; + legendGradientElem_alt.style.backgroundImage = horizontalBackgroundImageString; + + (async () => { + for (const elem of componentContainer.getElementsByClassName(paintIconClass)) { + const icon = AwcIcons.getAppearanceIcon(20); + elem.appendChild(icon); + } + const treeViewUtil = await visorState.treeViewUtilPromise; + treeViewUtil.addSelectionChangeListener(onSelectionChangeAsync); + visorState.addViewerClickedListener(async (node, ctrlKey, shiftKey) => { + // Only handle part selection in 'part' mode + if (visorState.getSelectionMode() !== 'part') { + return; + } + if (!ctrlKey && !shiftKey) { + const promises: Promise[] = []; + for (const row of treeViewUtil.rows) { + row.deselectRow(false); + const promise = row.node.setSelectedAsync(false); + promises.push(promise); + } + await Promise.all(promises); + } + if (node != null) { + const row = treeViewUtil.rowUtilsMap.get(node.id)!; + if (node.selected) { + row.deselectRow(false); + await node.setSelectedAsync(false); + } else { + row.selectRow(false); + await node.setSelectedAsync(true); + } + } + treeViewUtil.updateSelectedNodesArray(); + await onSelectionChangeAsync(treeViewUtil.selectedNodes); + }); + + // Reset part selection when switching away from 'part' mode + visorState.addSelectionModeChangedListener(async (_mode) => { + const promises: Promise[] = []; + for (const row of treeViewUtil.rows) { + row.deselectRow(false); + promises.push(row.node.setSelectedAsync(false)); + } + await Promise.all(promises); + treeViewUtil.updateSelectedNodesArray(); + await onSelectionChangeAsync([]); + await visorState.render(); + }); + + await onSelectionChangeAsync([]); + const util = new Panel_TopRight_Util( + expandPanel, + collapsePanel, + expandLegend, + collapseLegend, + getIsPanelCollapsed, + getIsLegendCollapsed, + (tabIndex: number) => { + if (tabIndex === 0) { + propertyTabElem.click(); + } else if (tabIndex === 1) { + legendTabElem.click(); + } + }, + () => tabIndex + ); + visorState.setPanelTopRightUtil(util); + onLoad(util); + })(); + + async function onSelectionChangeAsync(selectedNodes: VisorSceneNodeExtended[]) { + const actorNodes = selectedNodes.filter((node) => { + return node.isActorNode; + }); + if (actorNodes.length > 0) { + propertyPanelBodyElem.style.removeProperty('display'); + legendPanelBodyElem.style.removeProperty('display'); + propertyPanelNoPartsElem.style.display = 'none'; + legendPanelNoPartsElem.style.display = 'none'; + const aggregateSelectionInfo = + await AggregateSelectionInfo.getInstanceAsync(actorNodes); + + aggregateSelectionInfo.events.onSpectrumComponentChange = async (spectrumInfo) => { + await spectrumComponentChangeHandler(spectrumInfo); + await applySpectrumAsync(spectrumInfo?.currentComponentInfo); + }; + + aggregateSelectionInfo.events.onSpectrumChange = async (spectrumInfo) => { + await spectrumChangeHandler(spectrumInfo); + await applySpectrumAsync(spectrumInfo?.currentComponentInfo); + }; + + await spectrumChangeHandler(aggregateSelectionInfo.currentSpectrumInfo); + await spectrumComponentChangeHandler(aggregateSelectionInfo.currentSpectrumInfo); + + async function spectrumComponentChangeHandler( + spectrumInfo: AggregateSpectrumInfo | null | undefined + ) { + const componentInfo = spectrumInfo?.currentComponentInfo; + if (spectrumInfo == null || componentInfo == null) { + legendPanelNoRangeElem.style.removeProperty('display'); + legendPanelRangeContainer.style.display = 'none'; + legendOverlayElem.style.display = 'none'; + legendOverlayElem_alt.style.display = 'none'; + if (spectrumInfo === null) { + legendPanelNoRangeElem.innerHTML = `No data array is selected.`; + } else if (spectrumInfo === undefined) { + const msg = `Selected parts must all have the same data array and component`; + legendPanelNoRangeElem.innerHTML = `${msg} in order to edit min/max values.`; + } else if (componentInfo === null) { + legendPanelNoRangeElem.innerHTML = `No component is selected.`; + } else if (componentInfo === undefined) { + const msg = `Selected parts must all have the same data array and component`; + legendPanelNoRangeElem.innerHTML = `${msg} in order to edit min/max values.`; + } + } else { + componentInfo.events.onMinChange = () => { + updateMinLabel(componentInfo.displaySpectrumMin); + updateLegend(spectrumInfo); + }; + componentInfo.events.onMaxChange = () => { + updateMaxLabel(componentInfo.displaySpectrumMax); + updateLegend(spectrumInfo); + }; + updateMinLabel(componentInfo.displaySpectrumMin); + updateMaxLabel(componentInfo.displaySpectrumMax); + updateLegend(spectrumInfo); + } + } + + function updateLegend(spectrumInfo: AggregateSpectrumInfo) { + const componentInfo = spectrumInfo.currentComponentInfo; + if (componentInfo == null) { + throw new Error('componentInfo should not be null here'); + } + legendPanelNoRangeElem.style.display = 'none'; + legendPanelRangeContainer.style.removeProperty('display'); + legendOverlayElem.style.removeProperty('display'); + legendOverlayElem_alt.style.removeProperty('display'); + const spectrumMetadata = spectrumInfo.metadata; + const overlayTitle = ` +
+ + + + + + + + + + + + + + ${ + spectrumMetadata.numComponents > 1 + ? ` + + + + + ` + : `` + } +
Array Type:${escapeHtml(spectrumMetadata.type)}
Array Name:${escapeHtml(spectrumMetadata.name)}
Array Shape:${escapeHtml(spectrumMetadata.shape)}
Component:${escapeHtml(componentInfo.metadata.name)}
+
+ `; + legendOverlayTitleContainer.innerHTML = overlayTitle; + legendOverlayTitleContainer_alt.innerHTML = overlayTitle; + { + const min = componentInfo.displaySpectrumMin; + const max = componentInfo.displaySpectrumMax; + const minRounded = + typeof min === 'number' + ? fixValue(min) + : legendPanelMinInput.placeholder; + const maxRounded = + typeof max === 'number' + ? fixValue(max) + : legendPanelMaxInput.placeholder; + legendGradientMinTextContainer.innerHTML = minRounded; + legendGradientMinTextContainer_alt.innerHTML = minRounded; + legendGradientMaxTextContainer.innerHTML = maxRounded; + legendGradientMaxTextContainer_alt.innerHTML = maxRounded; + if (typeof min === 'number' && typeof max === 'number') { + const midNum = (max - min) / 2 + min; + const midRounded = fixValue(midNum); + legendGradientMidTextContainer.innerHTML = midRounded; + legendGradientMidTextContainer_alt.innerHTML = midRounded; + } else { + legendGradientMidTextContainer.innerHTML = ' '; + legendGradientMidTextContainer_alt.innerHTML = ' '; + } + } + } + + async function spectrumChangeHandler( + spectrumInfo: AggregateSpectrumInfo | null | undefined + ) { + await spectrumComponentChangeHandler(spectrumInfo); + componentSelectElem.options.length = 0; + if (spectrumInfo != null) { + constantRgbInputContainer.style.display = 'none'; + propertyPanelComponentContainer.style.removeProperty('display'); + propertyPanelNoComponentContainer.style.display = 'none'; + if (spectrumInfo.displayComponentId === undefined) { + const optionElem = document.createElement('option'); + optionElem.value = ''; + optionElem.text = ''; + componentSelectElem.options.add(optionElem); + } + for (const item of spectrumInfo.componentOptions.values()) { + const option = document.createElement('option'); + option.value = item.id.toString(); + option.text = item.metadata.name; + option.selected = item.id === spectrumInfo.displayComponentId; + componentSelectElem.options.add(option); + } + componentSelectElem.disabled = componentSelectElem.options.length === 1; + const originalOptionCount = componentSelectElem.options.length; + componentSelectElem.onchange = async () => { + if (spectrumInfo.displayComponentId === undefined) { + if (originalOptionCount === componentSelectElem.options.length) { + if (componentSelectElem.selectedIndex !== 0) { + componentSelectElem.options[0].remove(); + } + } + } + spectrumInfo.setDisplayComponentId(componentSelectElem.value); + }; + } else { + propertyPanelComponentContainer.style.display = 'none'; + if (spectrumInfo === null) { + constantRgbInputContainer.style.removeProperty('display'); + propertyPanelNoComponentContainer.style.display = 'none'; + await clearSpectrumAsync(); + } else if (spectrumInfo === undefined) { + constantRgbInputContainer.style.display = 'none'; + propertyPanelNoComponentContainer.style.removeProperty('display'); + const msg = `Selected parts must all have the same data array in order to`; + propertyPanelNoComponentContainer.innerHTML = `${msg} change the component.`; + } + } + } + + if (aggregateSelectionInfo.displayName === undefined) { + nameInputElem.style.display = 'none'; + nameDifferentElem.style.removeProperty('display'); + } else { + nameInputElem.value = escapeHtml(aggregateSelectionInfo.displayName); + nameInputElem.style.removeProperty('display'); + nameDifferentElem.style.display = 'none'; + } + if (aggregateSelectionInfo.displayOpacity === undefined) { + opacityRangeInputElem.style.display = 'none'; + opacityDifferentElem.style.removeProperty('display'); + opacityTextInputTable.style.visibility = 'hidden'; + opacityResetButton.onclick = async () => { + await applyOpacityAsync(1); + await onSelectionChangeAsync(actorNodes); + }; + } else { + if (aggregateSelectionInfo.displayOpacity == null) { + throw new Error(`displayOpacity should not be null here`); + } + const opacityPct = opacity01ToPercent(aggregateSelectionInfo.displayOpacity); + opacityRangeInputElem.value = opacityPct.toString(); + opacityTextInputElem.value = opacityPct.toString(); + opacityRangeInputElem.style.removeProperty('display'); + opacityDifferentElem.style.display = 'none'; + opacityTextInputTable.style.visibility = 'visible'; + } + if (aggregateSelectionInfo.displayDiffuseColor === undefined) { + diffuseColorInput.style.display = 'none'; + diffuseColorDifferentElem.style.removeProperty('display'); + diffuseColorDifferentElem.innerHTML = '(different)'; + } else { + diffuseColorInput.style.removeProperty('display'); + diffuseColorDifferentElem.style.display = 'none'; + if (aggregateSelectionInfo.displayDiffuseColor == null) { + throw new Error(`displayDiffuseColor should not be null here`); + } + diffuseColorInput.value = aggregateSelectionInfo.displayDiffuseColor; + } + opacityRangeInputElem.oninput = async () => { + // Range input is always a percent [0..100] + const percent = isNumeric(opacityRangeInputElem.value) + ? parseFloat(opacityRangeInputElem.value) + : 100; + opacityTextInputElem.value = Math.round(percent).toString(); + await applyOpacityAsync(opacityPercentTo01(percent)); + }; + opacityTextInputElem.oninput = async () => { + // Text input is shown as percent; tolerate decimals and clamp. + const raw = opacityTextInputElem.value?.trim(); + const percent = isNumeric(raw) ? parseFloat(raw) : 100; + const percentClamped = Math.min(100, Math.max(0, percent)); + opacityRangeInputElem.value = Math.round(percentClamped).toString(); + // Keep the text as the user typed (except empty), but apply clamped. + await applyOpacityAsync(opacityPercentTo01(percentClamped)); + }; + legendPanelMinInput.onkeyup = async (e) => { + e.key === 'Enter' && legendPanelApplyRangeButton.onclick!(null!); + }; + legendPanelMaxInput.onkeyup = async (e) => { + e.key === 'Enter' && legendPanelApplyRangeButton.onclick!(null!); + }; + legendPanelApplyRangeButton.onclick = async () => { + const componentInfo = + aggregateSelectionInfo.currentSpectrumInfo?.currentComponentInfo; + if (componentInfo == null) { + throw new Error( + `min/max should not be edited if the current component info is null or undefined` + ); + } else if (componentInfo.setDisplaySpectrumMin(legendPanelMinInput.value)) { + if (componentInfo.setDisplaySpectrumMax(legendPanelMaxInput.value)) { + const { + id: componentId, + spectrumInfo: { id: spectrumId }, + displaySpectrumMin: min, + displaySpectrumMax: max, + } = componentInfo; + await visorState.setSpectrumRangeAsync( + spectrumId, + componentId, + min!, + max! + ); + await visorState.render(); + } + } + }; + legendPanelMinResetButton.onclick = async (e) => { + e.preventDefault(); + const componentInfo = + aggregateSelectionInfo.currentSpectrumInfo?.currentComponentInfo; + if (componentInfo == null) { + throw new Error( + `min/max should not be edited if the current component info is null or undefined` + ); + } + updateMinLabel(componentInfo.displaySpectrumDefaultMin); + }; + legendPanelMaxResetButton.onclick = async (e) => { + e.preventDefault(); + const componentInfo = + aggregateSelectionInfo.currentSpectrumInfo?.currentComponentInfo; + if (componentInfo == null) { + throw new Error( + `min/max should not be edited if the current component info is null or undefined` + ); + } + updateMaxLabel(componentInfo.displaySpectrumDefaultMax); + }; + diffuseColorResetButton.onclick = async (e) => { + e.preventDefault(); + await resetDiffuseColorAsync(); + await onSelectionChangeAsync(actorNodes); + }; + diffuseColorInput.oninput = async (e) => { + e.preventDefault(); + await applyDiffuseColorAsync(diffuseColorInput.value); + }; + + async function resetDiffuseColorAsync() { + const promises = []; + for (const node of actorNodes) { + const promise = node.resetDiffuseColorAsync(); + promises.push(promise); + } + await Promise.all(promises); + await visorState.render(); + } + + async function applyDiffuseColorAsync(hex: string) { + const promises = []; + for (const node of actorNodes) { + const promise = node.setDiffuseColorHexAsync(hex); + promises.push(promise); + } + await Promise.all(promises); + await visorState.render(); + } + + async function applyOpacityAsync(opacity: number) { + const promises = []; + for (const node of actorNodes) { + const promise = node.setOpacityAsync(opacity); + promises.push(promise); + } + await Promise.all(promises); + await visorState.render(); + } + + async function clearSpectrumAsync() { + const promises = []; + for (const node of actorNodes) { + const promise = node.clearColorVariableAsync(); + promises.push(promise); + } + await Promise.all(promises); + await visorState.render(); + } + + async function applySpectrumAsync( + componentInfo?: AggregateSpectrumComponentInfo | null, + defaultMin?: boolean, + defaultMax?: boolean, + typing?: boolean + ) { + if (componentInfo == null) { + return; + } + const { + spectrumInfo, + displaySpectrumDefaultMin, + displaySpectrumDefaultMax, + displaySpectrumMin, + displaySpectrumMax, + } = componentInfo; + const min = + defaultMin === true ? displaySpectrumDefaultMin : displaySpectrumMin; + const max = + defaultMax === true ? displaySpectrumDefaultMax : displaySpectrumMax; + updateMinLabel(min, typing); + updateMaxLabel(max, typing); + updateLegend(spectrumInfo); + const promises = []; + for (const node of actorNodes) { + const promise = node.setColorVariableAsync( + spectrumInfo.id, + componentInfo.id + ); + promises.push(promise); + } + await Promise.all(promises); + await visorState.render(); + } + + variableSelectElem.options.length = 0; + variableSelectElem.append( + ...(() => { + const arr: HTMLOptionElement[] = []; + arr.push( + (() => { + const optionElem = document.createElement('option'); + optionElem.text = 'Constant'; + return optionElem; + })() + ); + if (aggregateSelectionInfo.displaySpectrumId === undefined) { + arr.unshift( + (() => { + const optionElem = document.createElement('option'); + optionElem.value = ''; + optionElem.text = ''; + return optionElem; + })() + ); + } + return arr; + })() + ); + aggregateSelectionInfo.spectrumOptions.forEach((item) => { + const { id, metadata } = item; + const { type, name, fullName, numComponents } = metadata; + const optionElem = document.createElement('option'); + optionElem.text = fullName; + optionElem.value = id.toString(); + optionElem.dataset.name = name; + optionElem.dataset.type = type; + optionElem.selected = id === aggregateSelectionInfo.displaySpectrumId; + variableSelectElem.options.add(optionElem); + }); + const originalOptionCount = variableSelectElem.options.length; + variableSelectElem.onchange = async () => { + if (aggregateSelectionInfo.displaySpectrumId === undefined) { + if (originalOptionCount === variableSelectElem.options.length) { + if (variableSelectElem.selectedIndex !== 0) { + variableSelectElem.options[0].remove(); + } + } + } + aggregateSelectionInfo.setDisplaySpectrumId(variableSelectElem.value); + }; + } else { + propertyPanelBodyElem.style.display = 'none'; + legendPanelBodyElem.style.display = 'none'; + propertyPanelNoPartsElem.style.removeProperty('display'); + propertyPanelNoPartsElem.innerHTML = `No part(s) selected.`; + legendPanelNoPartsElem.style.removeProperty('display'); + legendPanelNoPartsElem.innerHTML = `No part(s) selected.`; + legendOverlayElem.style.display = 'none'; + legendOverlayElem_alt.style.display = 'none'; + } + } + + // Dynamically constrain legend height and respond to resizes. + const updateLegendPosition = () => { + let clipAncestor: HTMLElement | null = componentContainer.parentElement; + while (clipAncestor && clipAncestor !== document.body) { + const oy = getComputedStyle(clipAncestor).overflowY; + if (oy === 'hidden' || oy === 'clip') break; + clipAncestor = clipAncestor.parentElement; + } + if (clipAncestor && clipAncestor !== document.body) { + const containerHeight = componentContainer.offsetHeight; + const scale = + containerHeight > 0 + ? componentContainer.getBoundingClientRect().height / containerHeight + : 1; + const clipRect = clipAncestor.getBoundingClientRect(); + const containerRect = componentContainer.getBoundingClientRect(); + // Space from component bottom to scaffold bottom, in unscaled CSS units + const available = (clipRect.bottom - containerRect.bottom) / scale - 20; + legendOverlayElem.style.maxHeight = `${Math.max(50, available)}px`; + } + }; + + const legendResizeObserver = new ResizeObserver(updateLegendPosition); + legendResizeObserver.observe(componentContainer); + window.addEventListener('resize', updateLegendPosition); + updateLegendPosition(); + + // cleanup observer/listener on unmount + return () => { + legendResizeObserver.disconnect(); + window.removeEventListener('resize', updateLegendPosition); + }; + }, []); + return ( +
+
+ + + + + + + + +
+
+ + + + + + + +
+ Part Properties +
+
+
+
+ + + + + + + +
+ Legend Settings +
+
+
+
+
+
+
+ + +
+ + + + + + + +
Color by Variable
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + +
Legend
+
+
+
+
+
+ Min +
+ + + + + + +
+
+ Mid +
+
+
+ Max +
+
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight_Util.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight_Util.tsx new file mode 100644 index 00000000..96576216 --- /dev/null +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight_Util.tsx @@ -0,0 +1,43 @@ +export class Panel_TopRight_Util { + constructor( + expandPanel: () => void, + collapsePanel: () => void, + expandLegend: () => void, + collapseLegend: () => void, + getIsPanelCollapsed: () => boolean, + getIsLegendCollapsed: () => boolean, + selectTab: (tabIndex: number) => void, + getTabIndex: () => number + ) { + this.expandPanel = expandPanel; + this.collapsePanel = collapsePanel; + this.expandLegend = expandLegend; + this.collapseLegend = collapseLegend; + this.#getIsPanelCollapsed = getIsPanelCollapsed; + this.#getIsLegendCollapsed = getIsLegendCollapsed; + this.selectTab = selectTab; + this.#getTabIndex = getTabIndex; + } + + readonly collapsePanel: () => void; + readonly expandPanel: () => void; + readonly collapseLegend: () => void; + readonly expandLegend: () => void; + readonly #getIsPanelCollapsed: () => boolean; + readonly #getIsLegendCollapsed: () => boolean; + + get isPanelCollapsed() { + return this.#getIsPanelCollapsed(); + } + + get isLegendCollapsed() { + return this.#getIsLegendCollapsed(); + } + + readonly selectTab: (tabIndex: number) => void; + readonly #getTabIndex: () => number; + + get tabIndex() { + return this.#getTabIndex(); + } +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx new file mode 100644 index 00000000..5f4fe9a0 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx @@ -0,0 +1,443 @@ +import { AggregateSelectionInfo } from '../aggregate/AggregateSelectionInfo.tsx'; +import type { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import type { + VisorSpectrumComponentMetadata, + VisorSpectrumInfo, +} from '../state/VisorSpectrumManager.tsx'; + +describe('AggregateSelectionInfo', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('returns empty aggregate values when there are no actor nodes', async () => { + const result = await AggregateSelectionInfo.getInstanceAsync([]); + + expect(result.displayName).toBeNull(); + expect(result.displayOpacity).toBeNull(); + expect(result.displaySpectrumId).toBeNull(); + expect(result.displayDiffuseColor).toBeNull(); + expect(result.currentSpectrumInfo).toBeNull(); + expect(result.spectrumOptions.size).toBe(0); + }); + + test('aggregates matching values from all actor nodes', async () => { + const spectrum = createSpectrum(10, [1]); + + const actorNodes = [ + createActorNode({ + name: 'mesh', + opacity: 0.5, + spectrumId: '10', + spectrumComponent: 1, + customDiffuseColorHex: '#123456', + spectra: [spectrum], + }), + createActorNode({ + name: 'mesh', + opacity: 0.5, + spectrumId: '10', + spectrumComponent: 1, + customDiffuseColorHex: '#123456', + spectra: [spectrum], + }), + ]; + + const result = await AggregateSelectionInfo.getInstanceAsync(actorNodes); + + expect(result.displayName).toBe('mesh'); + expect(result.displayOpacity).toBe(0.5); + expect(result.displaySpectrumId).toBe('10'); + expect(result.displayDiffuseColor).toBe('#123456'); + + expect(result.spectrumOptions.size).toBe(1); + expect(result.spectrumOptions.has('10')).toBe(true); + expect(result.currentSpectrumInfo).toBe(result.spectrumOptions.get('10')); + expect(result.currentSpectrumInfo?.id).toBe('10'); + }); + + test('stores each spectrum only once when several nodes expose it', async () => { + const spectrum = createSpectrum(10, [1]); + + const actorNodes = [ + createActorNode({ + spectra: [spectrum], + spectrumId: '10', + spectrumComponent: 1, + }), + createActorNode({ + spectra: [spectrum], + spectrumId: '10', + spectrumComponent: 1, + }), + createActorNode({ + spectra: [spectrum], + spectrumId: '10', + spectrumComponent: 1, + }), + ]; + + const result = await AggregateSelectionInfo.getInstanceAsync(actorNodes); + + expect(result.spectrumOptions.size).toBe(1); + expect([...result.spectrumOptions.keys()]).toEqual(['10']); + }); + + test('collects different spectrum options from the actor nodes', async () => { + const spectrum10 = createSpectrum(10, [1]); + const spectrum20 = createSpectrum(20, [1]); + + const actorNodes = [ + createActorNode({ + spectra: [spectrum10], + spectrumId: '10', + spectrumComponent: 1, + }), + createActorNode({ + spectra: [spectrum20], + spectrumId: '10', + spectrumComponent: 1, + }), + ]; + + const result = await AggregateSelectionInfo.getInstanceAsync(actorNodes); + + expect([...result.spectrumOptions.keys()]).toEqual(['10', '20']); + }); + + test('uses undefined for values that differ between actor nodes', async () => { + const spectrum10 = createSpectrum(10, [1]); + const spectrum20 = createSpectrum(20, [1]); + + const actorNodes = [ + createActorNode({ + name: 'mesh A', + opacity: 0.25, + spectrumId: '10', + spectrumComponent: 1, + customDiffuseColorHex: '#111111', + spectra: [spectrum10, spectrum20], + }), + createActorNode({ + name: 'mesh B', + opacity: 0.75, + spectrumId: '20', + spectrumComponent: 1, + customDiffuseColorHex: '#222222', + spectra: [spectrum10, spectrum20], + }), + ]; + + const result = await AggregateSelectionInfo.getInstanceAsync(actorNodes); + + expect(result.displayName).toBeUndefined(); + expect(result.displayOpacity).toBeUndefined(); + expect(result.displaySpectrumId).toBeUndefined(); + expect(result.displayDiffuseColor).toBeUndefined(); + expect(result.currentSpectrumInfo).toBeUndefined(); + }); + + test('preserves values that match while marking only differing values undefined', async () => { + const spectrum = createSpectrum(10, [1]); + + const actorNodes = [ + createActorNode({ + name: 'same name', + opacity: 0.25, + spectrumId: '10', + spectrumComponent: 1, + customDiffuseColorHex: '#123456', + spectra: [spectrum], + }), + createActorNode({ + name: 'same name', + opacity: 0.75, + spectrumId: '10', + spectrumComponent: 1, + customDiffuseColorHex: '#123456', + spectra: [spectrum], + }), + ]; + + const result = await AggregateSelectionInfo.getInstanceAsync(actorNodes); + + expect(result.displayName).toBe('same name'); + expect(result.displayOpacity).toBeUndefined(); + expect(result.displaySpectrumId).toBe('10'); + expect(result.displayDiffuseColor).toBe('#123456'); + }); + + test('warns when the common spectrum ID is not an available option', async () => { + const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const actorNode = createActorNode({ + spectrumId: '999', + spectrumComponent: 1, + spectra: [createSpectrum(10, [1])], + }); + + const result = await AggregateSelectionInfo.getInstanceAsync([actorNode]); + + expect(result.displaySpectrumId).toBe('999'); + expect(result.currentSpectrumInfo).toBeUndefined(); + expect(warningSpy).toHaveBeenCalledWith("invalid spectrumId: '999'"); + }); + + test('setDisplayName updates the displayed name', async () => { + const result = await AggregateSelectionInfo.getInstanceAsync([]); + + result.setDisplayName('updated name'); + + expect(result.displayName).toBe('updated name'); + + result.setDisplayName(null); + expect(result.displayName).toBeNull(); + + result.setDisplayName(undefined); + expect(result.displayName).toBeUndefined(); + }); + + test('setDisplayOpacity parses numeric strings', async () => { + const result = await AggregateSelectionInfo.getInstanceAsync([]); + + result.setDisplayOpacity('0.75'); + + expect(result.displayOpacity).toBe(0.75); + }); + + test('setDisplayOpacity accepts numbers, null, and undefined', async () => { + const result = await AggregateSelectionInfo.getInstanceAsync([]); + + result.setDisplayOpacity(0.25); + expect(result.displayOpacity).toBe(0.25); + + result.setDisplayOpacity(null); + expect(result.displayOpacity).toBeNull(); + + result.setDisplayOpacity(undefined); + expect(result.displayOpacity).toBeUndefined(); + }); + + test('setDisplayOpacity converts an invalid string to null', async () => { + const result = await AggregateSelectionInfo.getInstanceAsync([]); + + result.setDisplayOpacity('not numeric'); + + expect(result.displayOpacity).toBeNull(); + }); + + test('setDisplaySpectrumId selects an available spectrum', async () => { + const spectrum10 = createSpectrum(10, [1]); + const spectrum20 = createSpectrum(20, [1]); + + const result = await AggregateSelectionInfo.getInstanceAsync([ + createActorNode({ + spectrumId: '10', + spectrumComponent: 1, + spectra: [spectrum10, spectrum20], + }), + ]); + + const selected = result.setDisplaySpectrumId(20); + + expect(selected).toBe(result.spectrumOptions.get('20')); + expect(result.currentSpectrumInfo).toBe(result.spectrumOptions.get('20')); + expect(result.displaySpectrumId).toBe('20'); + }); + + test('setDisplaySpectrumId accepts a string ID', async () => { + const spectrum = createSpectrum(10, [1]); + + const result = await AggregateSelectionInfo.getInstanceAsync([ + createActorNode({ + spectrumId: null, + spectrumComponent: 1, + spectra: [spectrum], + }), + ]); + + const selected = result.setDisplaySpectrumId('10'); + + expect(selected).toBe(result.spectrumOptions.get('10')); + expect(result.displaySpectrumId).toBe('10'); + }); + + test('setDisplaySpectrumId returns null for an unavailable spectrum', async () => { + const spectrum = createSpectrum(10, [1]); + + const result = await AggregateSelectionInfo.getInstanceAsync([ + createActorNode({ + spectrumId: '10', + spectrumComponent: 1, + spectra: [spectrum], + }), + ]); + + const selected = result.setDisplaySpectrumId('999'); + + expect(selected).toBeNull(); + expect(result.currentSpectrumInfo).toBeNull(); + expect(result.displaySpectrumId).toBeNull(); + }); + + test('setDisplaySpectrumId preserves null and undefined', async () => { + const spectrum = createSpectrum(10, [1]); + + const result = await AggregateSelectionInfo.getInstanceAsync([ + createActorNode({ + spectrumId: '10', + spectrumComponent: 1, + spectra: [spectrum], + }), + ]); + + expect(result.setDisplaySpectrumId(null)).toBeNull(); + expect(result.currentSpectrumInfo).toBeNull(); + expect(result.displaySpectrumId).toBeNull(); + + expect(result.setDisplaySpectrumId(undefined)).toBeUndefined(); + expect(result.currentSpectrumInfo).toBeUndefined(); + expect(result.displaySpectrumId).toBeUndefined(); + }); + + test('setDisplaySpectrumId invokes onSpectrumChange', async () => { + const spectrum10 = createSpectrum(10, [1]); + const spectrum20 = createSpectrum(20, [1]); + + const result = await AggregateSelectionInfo.getInstanceAsync([ + createActorNode({ + spectrumId: '10', + spectrumComponent: 1, + spectra: [spectrum10, spectrum20], + }), + ]); + + const handler = jest.fn(); + result.events.onSpectrumChange = handler; + + const selected = result.setDisplaySpectrumId(20); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(selected); + }); + + test('setDisplaySpectrumId invokes the event with null for an invalid ID', async () => { + const spectrum = createSpectrum(10, [1]); + + const result = await AggregateSelectionInfo.getInstanceAsync([ + createActorNode({ + spectrumId: '10', + spectrumComponent: 1, + spectra: [spectrum], + }), + ]); + + const handler = jest.fn(); + result.events.onSpectrumChange = handler; + + result.setDisplaySpectrumId(999); + + expect(handler).toHaveBeenCalledWith(null); + }); + + test('events initially contain null handlers', async () => { + const result = await AggregateSelectionInfo.getInstanceAsync([]); + + expect(result.events.onSpectrumChange).toBeNull(); + expect(result.events.onSpectrumComponentChange).toBeNull(); + }); +}); + +interface SpectrumFixture { + metadata: VisorSpectrumInfo; + runtime: { + componentOptions: VisorSpectrumComponentMetadata[]; + getRangeInfo: jest.Mock; + }; +} + +interface ActorNodeOptions { + name?: string | null; + opacity?: number | null; + spectrumId?: string | null; + spectrumComponent?: number; + customDiffuseColorHex?: string | null; + spectra?: SpectrumFixture[]; +} + +function createSpectrum(id: number, componentIds: number[]): SpectrumFixture { + const componentOptions = componentIds.map((componentId) => ({ + id: componentId, + name: `Component ${componentId}`, + })) as VisorSpectrumComponentMetadata[]; + + const validIds = new Set(componentIds); + const idString = id.toString(); + + const metadata: VisorSpectrumInfo = { + id: idString, + type: 'POINT', + name: `spectrum-${idString}`, + shape: componentIds.length === 1 ? 'Scalar' : `Vector${componentIds.length}`, + fullName: `POINT - spectrum-${idString}`, + numComponents: componentIds.length, + componentOptions, + getRangeInfo: (componentId) => { + if (componentId == null || !validIds.has(componentId)) { + return null; + } + + return { + defaultRange: [0, 1], + customRange: [0, 1], + }; + }, + setCustomRange: jest.fn(), + }; + + return { + metadata, + runtime: { + componentOptions, + getRangeInfo: jest.fn((componentId: number) => { + if (!validIds.has(componentId)) { + return null; + } + + return { + defaultRange: [0, 1], + customRange: [0, 1], + }; + }), + }, + }; +} + +function createActorNode({ + name = 'mesh', + opacity = 1, + spectrumId = null, + spectrumComponent = 0, + customDiffuseColorHex = '#ffffff', + spectra = [], +}: ActorNodeOptions = {}): VisorSceneNodeExtended { + return { + name, + opacity, + spectrumId, + spectrumComponent, + customDiffuseColorHex, + + spectrumCollection: { + array: spectra.map((spectrum) => spectrum.metadata), + + getSpectrum: jest.fn((id: number | string) => { + const match = spectra.find( + (spectrum) => spectrum.metadata.id.toString() === id.toString() + ); + + return match?.runtime ?? null; + }), + }, + } as unknown as VisorSceneNodeExtended; +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumComponentInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumComponentInfo.test.tsx new file mode 100644 index 00000000..ee60188f --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumComponentInfo.test.tsx @@ -0,0 +1,672 @@ +import { AggregateSelectionInfo } from '../aggregate/AggregateSelectionInfo.tsx'; +import { AggregateSpectrumComponentInfo } from '../aggregate/AggregateSpectrumComponentInfo.tsx'; +import type { AggregateSpectrumInfo } from '../aggregate/AggregateSpectrumInfo.tsx'; +import type { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import type { + VisorSpectrumCollection, + VisorSpectrumComponentMetadata, + VisorSpectrumInfo, +} from '../state/VisorSpectrumManager.tsx'; +import type { FieldAssociation } from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; + +describe('AggregateSpectrumComponentInfo', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('exposes its component ID, metadata, and parent spectrum', async () => { + const parentSpectrum = await createParentSpectrum(); + const metadata = getComponentMetadata(parentSpectrum, 0); + + const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + [ + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [ + createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + }), + ], + }), + ], + parentSpectrum, + metadata + ); + + expect(result).toBeInstanceOf(AggregateSpectrumComponentInfo); + expect(result.id).toBe(0); + expect(result.metadata).toBe(metadata); + expect(result.spectrumInfo).toBe(parentSpectrum); + }); + + test('returns null aggregate values when there are no actor nodes', async () => { + const parentSpectrum = await createParentSpectrum(); + const metadata = getComponentMetadata(parentSpectrum, 0); + + const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + [], + parentSpectrum, + metadata + ); + + expect(result.displaySpectrumDefaultMin).toBeNull(); + expect(result.displaySpectrumDefaultMax).toBeNull(); + expect(result.displaySpectrumMin).toBeNull(); + expect(result.displaySpectrumMax).toBeNull(); + }); + + test('aggregates matching range values from all actor nodes', async () => { + const parentSpectrum = await createParentSpectrum(); + const metadata = getComponentMetadata(parentSpectrum, 0); + + const firstSpectrum = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), + }); + + const secondSpectrum = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), + }); + + const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + [ + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [firstSpectrum], + }), + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [secondSpectrum], + }), + ], + parentSpectrum, + metadata + ); + + expect(result.displaySpectrumDefaultMin).toBe(-10); + expect(result.displaySpectrumDefaultMax).toBe(10); + expect(result.displaySpectrumMin).toBe(-5); + expect(result.displaySpectrumMax).toBe(5); + }); + + test('marks only differing range fields as undefined', async () => { + const parentSpectrum = await createParentSpectrum(); + const metadata = getComponentMetadata(parentSpectrum, 0); + + const firstSpectrum = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), + }); + + const secondSpectrum = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-20, 10, -15, 5)]]), + }); + + const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + [ + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [firstSpectrum], + }), + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [secondSpectrum], + }), + ], + parentSpectrum, + metadata + ); + + expect(result.displaySpectrumDefaultMin).toBeUndefined(); + expect(result.displaySpectrumDefaultMax).toBe(10); + expect(result.displaySpectrumMin).toBeUndefined(); + expect(result.displaySpectrumMax).toBe(5); + }); + + test('sets all range values to undefined when a node has no matching spectrum', async () => { + const parentSpectrum = await createParentSpectrum(); + const metadata = getComponentMetadata(parentSpectrum, 0); + + const matchingSpectrum = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), + }); + + const unrelatedSpectrum = createSpectrum({ + id: 'POINT::temperature::1', + name: 'temperature', + shape: 'Scalar', + componentIds: [-1], + }); + + const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + [ + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [matchingSpectrum], + }), + createActorNode({ + spectrumId: unrelatedSpectrum.id, + spectrumComponent: -1, + spectra: [unrelatedSpectrum], + }), + ], + parentSpectrum, + metadata + ); + + expect(result.displaySpectrumDefaultMin).toBeUndefined(); + expect(result.displaySpectrumDefaultMax).toBeUndefined(); + expect(result.displaySpectrumMin).toBeUndefined(); + expect(result.displaySpectrumMax).toBeUndefined(); + }); + + test('sets all range values to undefined when a component range is missing', async () => { + const parentSpectrum = await createParentSpectrum(); + const metadata = getComponentMetadata(parentSpectrum, 0); + + const spectrumWithRange = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), + }); + + const spectrumWithoutRange = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + omittedRangeIds: [0], + }); + + const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + [ + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [spectrumWithRange], + }), + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [spectrumWithoutRange], + }), + ], + parentSpectrum, + metadata + ); + + expect(result.displaySpectrumDefaultMin).toBeUndefined(); + expect(result.displaySpectrumDefaultMax).toBeUndefined(); + expect(result.displaySpectrumMin).toBeUndefined(); + expect(result.displaySpectrumMax).toBeUndefined(); + }); + + test('stops aggregation after encountering a missing range', async () => { + const parentSpectrum = await createParentSpectrum(); + const metadata = getComponentMetadata(parentSpectrum, 0); + + const missingRangeSpectrum = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + omittedRangeIds: [0], + }); + + const laterSpectrum = createSpectrum({ + id: parentSpectrum.id, + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-100, 100, -50, 50)]]), + }); + + const laterGetRangeInfoSpy = jest.spyOn(laterSpectrum, 'getRangeInfo'); + + const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + [ + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [missingRangeSpectrum], + }), + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [laterSpectrum], + }), + ], + parentSpectrum, + metadata + ); + + expect(result.displaySpectrumDefaultMin).toBeUndefined(); + expect(result.displaySpectrumDefaultMax).toBeUndefined(); + expect(result.displaySpectrumMin).toBeUndefined(); + expect(result.displaySpectrumMax).toBeUndefined(); + + expect(laterGetRangeInfoSpy).not.toHaveBeenCalled(); + }); + + describe('setDisplaySpectrumMin', () => { + test('accepts a number', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMin(12.5); + + expect(success).toBe(true); + expect(result.displaySpectrumMin).toBe(12.5); + }); + + test('parses a numeric string', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMin('12.5'); + + expect(success).toBe(true); + expect(result.displaySpectrumMin).toBe(12.5); + }); + + test('uses parseFloat behavior for partially numeric strings', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMin('12.5px'); + + expect(success).toBe(true); + expect(result.displaySpectrumMin).toBe(12.5); + }); + + test('converts a nonnumeric string to null', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMin('not numeric'); + + expect(success).toBe(false); + expect(result.displaySpectrumMin).toBeNull(); + }); + + test('preserves null', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMin(null); + + expect(success).toBe(false); + expect(result.displaySpectrumMin).toBeNull(); + }); + + test('preserves undefined', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMin(undefined); + + expect(success).toBe(false); + expect(result.displaySpectrumMin).toBeUndefined(); + }); + + test('invokes onMinChange with the parsed value', async () => { + const result = await createComponentInfo(); + const handler = jest.fn(); + + result.events.onMinChange = handler; + + result.setDisplaySpectrumMin('25.5'); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(25.5); + }); + + test('invokes onMinChange with null for invalid input', async () => { + const result = await createComponentInfo(); + const handler = jest.fn(); + + result.events.onMinChange = handler; + + result.setDisplaySpectrumMin('invalid'); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(null); + }); + }); + + describe('setDisplaySpectrumMax', () => { + test('accepts a number', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMax(87.5); + + expect(success).toBe(true); + expect(result.displaySpectrumMax).toBe(87.5); + }); + + test('parses a numeric string', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMax('87.5'); + + expect(success).toBe(true); + expect(result.displaySpectrumMax).toBe(87.5); + }); + + test('uses parseFloat behavior for partially numeric strings', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMax('87.5px'); + + expect(success).toBe(true); + expect(result.displaySpectrumMax).toBe(87.5); + }); + + test('converts a nonnumeric string to null', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMax('not numeric'); + + expect(success).toBe(false); + expect(result.displaySpectrumMax).toBeNull(); + }); + + test('preserves null', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMax(null); + + expect(success).toBe(false); + expect(result.displaySpectrumMax).toBeNull(); + }); + + test('preserves undefined', async () => { + const result = await createComponentInfo(); + + const success = result.setDisplaySpectrumMax(undefined); + + expect(success).toBe(false); + expect(result.displaySpectrumMax).toBeUndefined(); + }); + + test('invokes onMaxChange with the parsed value', async () => { + const result = await createComponentInfo(); + const handler = jest.fn(); + + result.events.onMaxChange = handler; + + result.setDisplaySpectrumMax('75.5'); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(75.5); + }); + + test('invokes onMaxChange with null for invalid input', async () => { + const result = await createComponentInfo(); + const handler = jest.fn(); + + result.events.onMaxChange = handler; + + result.setDisplaySpectrumMax('invalid'); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(null); + }); + }); + + test('events initially contain null handlers', async () => { + const result = await createComponentInfo(); + + expect(result.events.onMinChange).toBeNull(); + expect(result.events.onMaxChange).toBeNull(); + }); + + test('setters replace custom aggregate values without changing defaults', async () => { + const result = await createComponentInfo(); + + expect(result.displaySpectrumDefaultMin).toBe(-10); + expect(result.displaySpectrumDefaultMax).toBe(10); + expect(result.displaySpectrumMin).toBe(-5); + expect(result.displaySpectrumMax).toBe(5); + + result.setDisplaySpectrumMin(-2); + result.setDisplaySpectrumMax(2); + + expect(result.displaySpectrumMin).toBe(-2); + expect(result.displaySpectrumMax).toBe(2); + expect(result.displaySpectrumDefaultMin).toBe(-10); + expect(result.displaySpectrumDefaultMax).toBe(10); + }); +}); + +interface RangeInfo { + defaultRange: [number, number]; + customRange: [number, number]; +} + +interface CreateSpectrumOptions { + id: string; + componentIds: number[]; + name?: string; + type?: FieldAssociation; + shape?: string; + ranges?: Map; + omittedRangeIds?: number[]; +} + +interface CreateActorNodeOptions { + spectrumId: string | null; + spectrumComponent: number; + spectra: VisorSpectrumInfo[]; +} + +async function createParentSpectrum(): Promise { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + shape: 'Vector3', + componentIds: [-1, 0, 1, 2], + }); + + const actorNode = createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }); + + const selection = await AggregateSelectionInfo.getInstanceAsync([actorNode]); + + expect(selection.currentSpectrumInfo).not.toBeNull(); + expect(selection.currentSpectrumInfo).not.toBeUndefined(); + + return selection.currentSpectrumInfo!; +} + +async function createComponentInfo(): Promise { + const parentSpectrum = await createParentSpectrum(); + + const metadata = getComponentMetadata(parentSpectrum, 0); + + const actorSpectrum = createSpectrum({ + id: parentSpectrum.id, + name: 'velocity', + shape: 'Vector3', + componentIds: [-1, 0, 1, 2], + ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), + }); + + return AggregateSpectrumComponentInfo.getInstanceAsync( + [ + createActorNode({ + spectrumId: parentSpectrum.id, + spectrumComponent: 0, + spectra: [actorSpectrum], + }), + ], + parentSpectrum, + metadata + ); +} + +function getComponentMetadata( + spectrumInfo: AggregateSpectrumInfo, + componentId: number +): VisorSpectrumComponentMetadata { + const metadata = spectrumInfo.metadata.componentOptions.find((item) => item.id === componentId); + + if (metadata == null) { + throw new Error(`component metadata not found: ${componentId}`); + } + + return metadata; +} + +function createSpectrum({ + id, + componentIds, + name = 'velocity', + type = 'POINT', + shape = 'Vector3', + ranges = new Map(), + omittedRangeIds = [], +}: CreateSpectrumOptions): VisorSpectrumInfo { + const componentOptions = createComponentOptions(componentIds); + + const rangeState = new Map(); + const omitted = new Set(omittedRangeIds); + + for (const componentId of componentIds) { + if (omitted.has(componentId)) { + continue; + } + + const suppliedRange = ranges.get(componentId); + + rangeState.set( + componentId, + suppliedRange != null ? cloneRangeInfo(suppliedRange) : createRange(0, 1, 0, 1) + ); + } + + for (const [componentId, range] of ranges) { + if (!omitted.has(componentId)) { + rangeState.set(componentId, cloneRangeInfo(range)); + } + } + + const numComponents = componentIds.filter((componentId) => componentId >= 0).length; + + return { + id, + type, + name, + shape, + fullName: `${type} - ${name} (${shape})`, + numComponents, + componentOptions, + + getRangeInfo(component) { + if (component == null) { + return null; + } + + const range = rangeState.get(component); + + if (range == null) { + return null; + } + + return cloneRangeInfo(range); + }, + + setCustomRange(component, min, max) { + const range = rangeState.get(component); + + if (range != null) { + range.customRange[0] = min; + range.customRange[1] = max; + } + }, + }; +} + +function createSpectrumCollection(spectra: VisorSpectrumInfo[]): VisorSpectrumCollection { + const array = [...spectra]; + + const map = new Map( + array.map((spectrum) => [spectrum.id, spectrum]) + ); + + return { + array, + + getSpectrum(id: string | null) { + if (id == null) { + return null; + } + + return map.get(id) ?? null; + }, + }; +} + +function createActorNode({ + spectrumId, + spectrumComponent, + spectra, +}: CreateActorNodeOptions): VisorSceneNodeExtended { + return { + name: 'mesh', + opacity: 1, + spectrumId, + spectrumComponent, + customDiffuseColorHex: '#ffffff', + spectrumCollection: createSpectrumCollection(spectra), + } as unknown as VisorSceneNodeExtended; +} + +function createComponentOptions(componentIds: number[]): VisorSpectrumComponentMetadata[] { + return componentIds.map((id) => ({ + id, + name: getComponentName(id), + })); +} + +function getComponentName(id: number): string { + switch (id) { + case -1: + return 'Magnitude'; + case 0: + return 'X'; + case 1: + return 'Y'; + case 2: + return 'Z'; + case 3: + return 'W'; + default: + return `Component ${id}`; + } +} + +function createRange( + defaultMin: number, + defaultMax: number, + customMin: number, + customMax: number +): RangeInfo { + return { + defaultRange: [defaultMin, defaultMax], + customRange: [customMin, customMax], + }; +} + +function cloneRangeInfo(range: RangeInfo): RangeInfo { + return { + defaultRange: [...range.defaultRange], + customRange: [...range.customRange], + }; +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumInfo.test.tsx new file mode 100644 index 00000000..2a9ee2ce --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumInfo.test.tsx @@ -0,0 +1,678 @@ +import { AggregateSelectionInfo } from '../aggregate/AggregateSelectionInfo.tsx'; +import { AggregateSpectrumInfo } from '../aggregate/AggregateSpectrumInfo.tsx'; +import type { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import type { + VisorSpectrumCollection, + VisorSpectrumComponentMetadata, + VisorSpectrumInfo, +} from '../state/VisorSpectrumManager.tsx'; +import type { FieldAssociation } from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; + +describe('AggregateSpectrumInfo', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('exposes its spectrum ID and metadata', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo; + + expect(result).toBeInstanceOf(AggregateSpectrumInfo); + expect(result?.id).toBe('POINT::velocity::3'); + expect(result?.metadata).toBe(spectrum); + }); + + test('creates one aggregate component option for each metadata component', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + expect([...result.componentOptions.keys()]).toEqual(['-1', '0', '1', '2']); + + expect(result.componentOptions.get('-1')?.id).toBe(-1); + expect(result.componentOptions.get('0')?.id).toBe(0); + expect(result.componentOptions.get('1')?.id).toBe(1); + expect(result.componentOptions.get('2')?.id).toBe(2); + }); + + test('associates aggregate components with their metadata', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 1, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + const component = result.componentOptions.get('1')!; + + expect(component.metadata).toBe(spectrum.componentOptions[2]); + expect(component.spectrumInfo).toBe(result); + }); + + test('selects the common component used by all actor nodes', async () => { + const firstSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const secondSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: firstSpectrum.id, + spectrumComponent: 1, + spectra: [firstSpectrum], + }), + createActorNode({ + spectrumId: secondSpectrum.id, + spectrumComponent: 1, + spectra: [secondSpectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + expect(result.displayComponentId).toBe(1); + expect(result.currentComponentInfo).toBe(result.componentOptions.get('1')); + }); + + test('uses undefined when actor nodes have different components', async () => { + const firstSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const secondSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: firstSpectrum.id, + spectrumComponent: 0, + spectra: [firstSpectrum], + }), + createActorNode({ + spectrumId: secondSpectrum.id, + spectrumComponent: 1, + spectra: [secondSpectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + expect(result.displayComponentId).toBeUndefined(); + expect(result.currentComponentInfo).toBeUndefined(); + }); + + test('defaults to the first component option when the selected component has no range', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 999, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + expect(result.displayComponentId).toBe(-1); + expect(result.currentComponentInfo).toBe(result.componentOptions.get('-1')); + }); + + test("aggregates component ranges from each actor node's spectrum collection", async () => { + const firstSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + ranges: new Map([ + [-1, createRange(0, 100, 10, 90)], + [0, createRange(-10, 10, -5, 5)], + [1, createRange(-20, 20, -15, 15)], + [2, createRange(-30, 30, -25, 25)], + ]), + }); + + const secondSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + ranges: new Map([ + [-1, createRange(0, 100, 10, 90)], + [0, createRange(-10, 10, -5, 5)], + [1, createRange(-20, 20, -15, 15)], + [2, createRange(-30, 30, -25, 25)], + ]), + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: firstSpectrum.id, + spectrumComponent: 1, + spectra: [firstSpectrum], + }), + createActorNode({ + spectrumId: secondSpectrum.id, + spectrumComponent: 1, + spectra: [secondSpectrum], + }), + ]); + + const component = selection.currentSpectrumInfo!.currentComponentInfo!; + + expect(component.displaySpectrumDefaultMin).toBe(-20); + expect(component.displaySpectrumDefaultMax).toBe(20); + expect(component.displaySpectrumMin).toBe(-15); + expect(component.displaySpectrumMax).toBe(15); + }); + + test('marks differing component ranges as undefined', async () => { + const firstSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0], + ranges: new Map([ + [-1, createRange(0, 100, 10, 90)], + [0, createRange(-10, 10, -5, 5)], + ]), + }); + + const secondSpectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0], + ranges: new Map([ + [-1, createRange(0, 100, 10, 90)], + [0, createRange(-20, 20, -15, 5)], + ]), + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: firstSpectrum.id, + spectrumComponent: 0, + spectra: [firstSpectrum], + }), + createActorNode({ + spectrumId: secondSpectrum.id, + spectrumComponent: 0, + spectra: [secondSpectrum], + }), + ]); + + const component = selection.currentSpectrumInfo!.currentComponentInfo!; + + expect(component.displaySpectrumDefaultMin).toBeUndefined(); + expect(component.displaySpectrumDefaultMax).toBeUndefined(); + expect(component.displaySpectrumMin).toBeUndefined(); + expect(component.displaySpectrumMax).toBe(5); + }); + + test('warns when a displayed component has range data but no component option', async () => { + const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1], + ranges: new Map([ + [-1, createRange(0, 100, 0, 100)], + [0, createRange(-10, 10, -10, 10)], + [1, createRange(-20, 20, -20, 20)], + [999, createRange(-1, 1, -1, 1)], + ]), + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 999, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + expect(result.displayComponentId).toBe(999); + expect(result.currentComponentInfo).toBeUndefined(); + expect(warningSpy).toHaveBeenCalledWith("invalid componentId: '999'"); + }); + + test('setDisplayComponentId selects a component by number', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + const selected = result.setDisplayComponentId(2); + + expect(selected).toBe(result.componentOptions.get('2')); + expect(result.currentComponentInfo).toBe(selected); + expect(result.displayComponentId).toBe(2); + }); + + test('setDisplayComponentId accepts a string ID', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + const selected = result.setDisplayComponentId('1'); + + expect(selected).toBe(result.componentOptions.get('1')); + expect(result.displayComponentId).toBe(1); + }); + + test('setDisplayComponentId returns null for an unavailable component', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + const selected = result.setDisplayComponentId(999); + + expect(selected).toBeNull(); + expect(result.currentComponentInfo).toBeNull(); + expect(result.displayComponentId).toBeNull(); + }); + + test('setDisplayComponentId preserves null', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + expect(result.setDisplayComponentId(null)).toBeNull(); + expect(result.currentComponentInfo).toBeNull(); + expect(result.displayComponentId).toBeNull(); + }); + + test('setDisplayComponentId preserves undefined', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + expect(result.setDisplayComponentId(undefined)).toBeUndefined(); + expect(result.currentComponentInfo).toBeUndefined(); + expect(result.displayComponentId).toBeUndefined(); + }); + + test('setDisplayComponentId invokes onSpectrumComponentChange', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + const handler = jest.fn(); + + selection.events.onSpectrumComponentChange = handler; + + result.setDisplayComponentId(1); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(result); + }); + + test('throws when changing a component while the current spectrum is null', async () => { + const spectrum = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: spectrum.id, + spectrumComponent: 0, + spectra: [spectrum], + }), + ]); + + const result = selection.currentSpectrumInfo!; + + selection.setDisplaySpectrumId(null); + + expect(() => { + result.setDisplayComponentId(1); + }).toThrow('component should not be changed when the current spectrum is null'); + }); + + test('throws when changing a component on a spectrum that is not current', async () => { + const velocity = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const acceleration = createSpectrum({ + id: 'POINT::acceleration::3', + name: 'acceleration', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: velocity.id, + spectrumComponent: 0, + spectra: [velocity, acceleration], + }), + ]); + + const nonCurrentSpectrum = selection.spectrumOptions.get(acceleration.id)!; + + expect(() => { + nonCurrentSpectrum.setDisplayComponentId(1); + }).toThrow('component should not be changed on a spectrum that is not current'); + }); + + test('allows component changes after a different spectrum becomes current', async () => { + const velocity = createSpectrum({ + id: 'POINT::velocity::3', + name: 'velocity', + componentIds: [-1, 0, 1, 2], + }); + + const acceleration = createSpectrum({ + id: 'POINT::acceleration::3', + name: 'acceleration', + componentIds: [-1, 0, 1, 2], + }); + + const selection = await createSelection([ + createActorNode({ + spectrumId: velocity.id, + spectrumComponent: 0, + spectra: [velocity, acceleration], + }), + ]); + + const accelerationInfo = selection.setDisplaySpectrumId(acceleration.id)!; + + expect(() => { + accelerationInfo.setDisplayComponentId(2); + }).not.toThrow(); + + expect(accelerationInfo.displayComponentId).toBe(2); + expect(accelerationInfo.currentComponentInfo).toBe( + accelerationInfo.componentOptions.get('2') + ); + }); +}); + +interface RangeInfo { + defaultRange: number[]; + customRange: number[]; +} + +interface CreateSpectrumOptions { + id: string; + name: string; + componentIds: number[]; + type?: FieldAssociation; + shape?: string; + ranges?: Map; +} + +interface CreateActorNodeOptions { + spectrumId: string | null; + spectrumComponent: number; + spectra: VisorSpectrumInfo[]; +} + +async function createSelection( + actorNodes: VisorSceneNodeExtended[] +): Promise { + return AggregateSelectionInfo.getInstanceAsync(actorNodes); +} + +function createSpectrum({ + id, + name, + componentIds, + type = 'POINT', + shape = 'Vector3', + ranges, +}: CreateSpectrumOptions): VisorSpectrumInfo { + const componentOptions = createComponentOptions(componentIds); + + const rangeState = new Map(); + + for (const componentId of componentIds) { + const suppliedRange = ranges?.get(componentId); + + rangeState.set( + componentId, + suppliedRange != null ? cloneRangeInfo(suppliedRange) : createRange(0, 1, 0, 1) + ); + } + + // Allow tests to provide range data for a component that is not + // present in componentOptions. + if (ranges != null) { + for (const [componentId, range] of ranges) { + if (!rangeState.has(componentId)) { + rangeState.set(componentId, cloneRangeInfo(range)); + } + } + } + + const numComponents = componentIds.filter((id) => id >= 0).length; + + return { + id, + type, + name, + shape, + fullName: `${type} - ${name} (${shape})`, + numComponents, + componentOptions, + + getRangeInfo(component) { + if (component == null) { + return null; + } + + const range = rangeState.get(component); + + if (range == null) { + return null; + } + + return cloneRangeInfo(range); + }, + + setCustomRange(component, min, max) { + const range = rangeState.get(component); + + if (range != null) { + range.customRange[0] = min; + range.customRange[1] = max; + } + }, + }; +} + +function createSpectrumCollection(spectra: VisorSpectrumInfo[]): VisorSpectrumCollection { + const array = [...spectra]; + const map = new Map(array.map((spectrum) => [spectrum.id, spectrum])); + + return { + array, + + getSpectrum(id: string | null) { + if (id == null) { + return null; + } + + return map.get(id) ?? null; + }, + }; +} + +function createActorNode({ + spectrumId, + spectrumComponent, + spectra, +}: CreateActorNodeOptions): VisorSceneNodeExtended { + return { + name: 'mesh', + opacity: 1, + spectrumId, + spectrumComponent, + customDiffuseColorHex: '#ffffff', + spectrumCollection: createSpectrumCollection(spectra), + } as unknown as VisorSceneNodeExtended; +} + +function createComponentOptions(componentIds: number[]): VisorSpectrumComponentMetadata[] { + return componentIds.map((id) => ({ + id, + name: getComponentName(id), + })); +} + +function getComponentName(id: number): string { + switch (id) { + case -1: + return 'Magnitude'; + case 0: + return 'X'; + case 1: + return 'Y'; + case 2: + return 'Z'; + case 3: + return 'W'; + default: + return `Component ${id}`; + } +} + +function createRange( + defaultMin: number, + defaultMax: number, + customMin: number, + customMax: number +): RangeInfo { + return { + defaultRange: [defaultMin, defaultMax], + customRange: [customMin, customMax], + }; +} + +function cloneRangeInfo(range: RangeInfo): RangeInfo { + return { + defaultRange: [...range.defaultRange], + customRange: [...range.customRange], + }; +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/FpsCounter.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/FpsCounter.test.tsx new file mode 100644 index 00000000..4d136771 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/FpsCounter.test.tsx @@ -0,0 +1,381 @@ +/** @jest-environment jsdom */ + +import { FpsCounter } from '../components/FpsCounter'; + +describe('FpsCounter', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + document.body.innerHTML = ''; + jest.restoreAllMocks(); + }); + + describe('constructor', () => { + test('appends the FPS display to the supplied container', () => { + const container = createContainer(); + + new FpsCounter(container, '10px'); + + expect(container.children).toHaveLength(1); + expect(container.firstElementChild).toBeInstanceOf(HTMLDivElement); + }); + + test('applies the supplied inset', () => { + const container = createContainer(); + + new FpsCounter(container, '10px 20px 30px 40px'); + + const display = getDisplay(container); + + expect(display.style.inset).toBe('10px 20px 30px 40px'); + }); + + test('applies the expected display styles', () => { + const container = createContainer(); + + new FpsCounter(container, '5px'); + + const style = getDisplay(container).style; + + expect(style.position).toBe('absolute'); + expect(style.inset).toBe('5px'); + expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0.75)'); + expect(style.color).toBe('rgb(255, 255, 255)'); + expect(style.padding).toBe('10px'); + expect(style.zIndex).toBe('999'); + expect(style.minWidth).toBe('16ch'); + expect(style.display).toBe('none'); + }); + + test('creates the expected FPS table', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + const display = getDisplay(container); + const table = display.querySelector('table'); + const cells = display.querySelectorAll('td'); + + expect(table).not.toBeNull(); + expect(cells).toHaveLength(2); + expect(cells[0].textContent).toBe('FPS:'); + expect(cells[0].classList.contains('text-left')).toBe(true); + expect(cells[1].classList.contains('text-right')).toBe(true); + expect(cells[1].textContent).toBe(''); + }); + + test('is hidden initially', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + expect(getDisplay(container).style.display).toBe('none'); + }); + }); + + describe('setValue', () => { + test('displays a numeric value', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + + counter.setValue(60); + + expect(getValueCell(container).textContent).toBe('60'); + }); + + test('displays a string value', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + + counter.setValue('59.8'); + + expect(getValueCell(container).textContent).toBe('59.8'); + }); + + test('replaces the previous value', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + + counter.setValue(30); + counter.setValue(60); + + expect(getValueCell(container).textContent).toBe('60'); + }); + + test('writes string values as HTML', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + + counter.setValue('60'); + + const valueCell = getValueCell(container); + + expect(valueCell.querySelector('strong')?.textContent).toBe('60'); + }); + }); + + describe('keyboard shortcut', () => { + test('shows the display when Ctrl+F10 is pressed', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe(''); + }); + + test('hides the display when Ctrl+F10 is pressed again after the debounce period', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe(''); + + jest.advanceTimersByTime(250); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe('none'); + }); + + test('ignores repeated Ctrl+F10 presses during the debounce period', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe(''); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe(''); + }); + + test('allows toggling again after 250 milliseconds', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + jest.advanceTimersByTime(249); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe(''); + + jest.advanceTimersByTime(1); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe('none'); + }); + + test('does nothing when F10 is pressed without Ctrl', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: false, + }); + + expect(getDisplay(container).style.display).toBe('none'); + }); + + test('does nothing when Ctrl is pressed with another key', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + dispatchKeyDown({ + key: 'F9', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe('none'); + }); + + test('matches the F10 key case-sensitively', () => { + const container = createContainer(); + + new FpsCounter(container, '0'); + + dispatchKeyDown({ + key: 'f10', + ctrlKey: true, + }); + + expect(getDisplay(container).style.display).toBe('none'); + }); + }); + + describe('dispose', () => { + test('removes the FPS display from the DOM', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + + expect(container.children).toHaveLength(1); + + counter.dispose(); + + expect(container.children).toHaveLength(0); + }); + + test('removes the keyboard event listener', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + const display = getDisplay(container); + + counter.dispose(); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(display.style.display).toBe('none'); + }); + + test('can be called more than once', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + + expect(() => { + counter.dispose(); + counter.dispose(); + }).not.toThrow(); + + expect(container.children).toHaveLength(0); + }); + + test('setValue can still update the detached display', () => { + const container = createContainer(); + const counter = new FpsCounter(container, '0'); + const valueCell = getValueCell(container); + + counter.dispose(); + counter.setValue(120); + + expect(valueCell.textContent).toBe('120'); + expect(valueCell.isConnected).toBe(false); + }); + }); + + describe('multiple counters', () => { + test('creates an independent display for each counter', () => { + const firstContainer = createContainer(); + const secondContainer = createContainer(); + + const firstCounter = new FpsCounter(firstContainer, '5px'); + const secondCounter = new FpsCounter(secondContainer, '10px'); + + firstCounter.setValue(30); + secondCounter.setValue(60); + + expect(getValueCell(firstContainer).textContent).toBe('30'); + expect(getValueCell(secondContainer).textContent).toBe('60'); + expect(getDisplay(firstContainer).style.inset).toBe('5px'); + expect(getDisplay(secondContainer).style.inset).toBe('10px'); + }); + + test('Ctrl+F10 toggles all active counters', () => { + const firstContainer = createContainer(); + const secondContainer = createContainer(); + + new FpsCounter(firstContainer, '0'); + new FpsCounter(secondContainer, '0'); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(getDisplay(firstContainer).style.display).toBe(''); + expect(getDisplay(secondContainer).style.display).toBe(''); + }); + + test('disposing one counter leaves the other counter active', () => { + const firstContainer = createContainer(); + const secondContainer = createContainer(); + + const firstCounter = new FpsCounter(firstContainer, '0'); + new FpsCounter(secondContainer, '0'); + + firstCounter.dispose(); + + dispatchKeyDown({ + key: 'F10', + ctrlKey: true, + }); + + expect(firstContainer.children).toHaveLength(0); + expect(getDisplay(secondContainer).style.display).toBe(''); + }); + }); +}); + +function createContainer(): HTMLDivElement { + const container = document.createElement('div'); + document.body.appendChild(container); + return container; +} + +function getDisplay(container: HTMLElement): HTMLDivElement { + const display = container.firstElementChild; + + expect(display).toBeInstanceOf(HTMLDivElement); + + return display as HTMLDivElement; +} + +function getValueCell(container: HTMLElement): HTMLTableCellElement { + const cells = container.querySelectorAll('td'); + + expect(cells).toHaveLength(2); + + return cells[1]; +} + +function dispatchKeyDown({ key, ctrlKey }: { key: string; ctrlKey: boolean }): void { + window.dispatchEvent( + new KeyboardEvent('keydown', { + key, + ctrlKey, + bubbles: true, + }) + ); +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js b/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js new file mode 100644 index 00000000..305e41a0 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js @@ -0,0 +1,634 @@ +/** @jest-environment jsdom */ + +import { + completeAssign, + escapeHtml, + formatNumber, + getCookie, + getPromiseResolver, + hashString, + isNumeric, + lazy, + randomId, + randomInt, + roundFloat, + setScale, + tryParseFloat, +} from '../utils/JsHelpers.js'; + +/** + * getRootFontSize caches module-level state, so each test needs a fresh + * instance of the module. + * + * @returns {Promise<(fallback: number) => number>} + */ +async function loadFreshGetRootFontSize() { + jest.resetModules(); + const module = await import('../utils/JsHelpers.js'); + return module.getRootFontSize; +} + +function clearCookies() { + document.cookie.split(';').forEach((cookie) => { + const separator = cookie.indexOf('='); + const name = separator >= 0 ? cookie.slice(0, separator).trim() : cookie.trim(); + + if (name) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; + } + }); +} + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('getRootFontSize', () => { + test.each([undefined, null, '16', {}, -1, -0.01])( + 'throws when fallback is invalid: %p', + async (fallback) => { + const getRootFontSize = await loadFreshGetRootFontSize(); + const getComputedStyleSpy = jest.spyOn(globalThis, 'getComputedStyle'); + + expect(() => getRootFontSize(fallback)).toThrow( + 'fallback must be a number 0 or greater' + ); + expect(getComputedStyleSpy).not.toHaveBeenCalled(); + } + ); + + test('returns the computed root font size when it is a pixel value', async () => { + const getRootFontSize = await loadFreshGetRootFontSize(); + const getPropertyValue = jest.fn().mockReturnValue('18.5px'); + + const getComputedStyleSpy = jest + .spyOn(globalThis, 'getComputedStyle') + .mockReturnValue({ getPropertyValue }); + + expect(getRootFontSize(16)).toBe(18.5); + expect(getComputedStyleSpy).toHaveBeenCalledWith(document.documentElement, null); + expect(getPropertyValue).toHaveBeenCalledWith('font-size'); + }); + + test('accepts a zero-pixel root font size', async () => { + const getRootFontSize = await loadFreshGetRootFontSize(); + + jest.spyOn(globalThis, 'getComputedStyle').mockReturnValue({ + getPropertyValue: jest.fn().mockReturnValue('0px'), + }); + + expect(getRootFontSize(16)).toBe(0); + }); + + test('caches a valid computed font size', async () => { + const getRootFontSize = await loadFreshGetRootFontSize(); + const getPropertyValue = jest.fn().mockReturnValue('18px'); + + const getComputedStyleSpy = jest + .spyOn(globalThis, 'getComputedStyle') + .mockReturnValue({ getPropertyValue }); + + expect(getRootFontSize(16)).toBe(18); + + getPropertyValue.mockReturnValue('24px'); + + expect(getRootFontSize(12)).toBe(18); + expect(getComputedStyleSpy).toHaveBeenCalledTimes(1); + expect(getPropertyValue).toHaveBeenCalledTimes(1); + }); + + test('warns and returns the fallback when the font size is not in pixels', async () => { + const getRootFontSize = await loadFreshGetRootFontSize(); + const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const getComputedStyleSpy = jest.spyOn(globalThis, 'getComputedStyle').mockReturnValue({ + getPropertyValue: jest.fn().mockReturnValue('1.25rem'), + }); + + expect(getRootFontSize(16)).toBe(16); + expect(warningSpy).toHaveBeenCalledWith( + 'TreeView warning: computed root element font size ' + + "'1.25rem' is not a pixel value. Using fallback." + ); + expect(getComputedStyleSpy).toHaveBeenCalledTimes(1); + }); + + test('uses the current fallback on each call after a non-pixel value is found', async () => { + const getRootFontSize = await loadFreshGetRootFontSize(); + + const getComputedStyleSpy = jest.spyOn(globalThis, 'getComputedStyle').mockReturnValue({ + getPropertyValue: jest.fn().mockReturnValue('normal'), + }); + + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + expect(getRootFontSize(16)).toBe(16); + expect(getRootFontSize(20)).toBe(20); + expect(getComputedStyleSpy).toHaveBeenCalledTimes(1); + }); +}); + +describe('hashString', () => { + test('returns the same hash for the same string', () => { + expect(hashString('TreeView')).toBe(hashString('TreeView')); + }); + + test('produces stable regression values', () => { + expect(hashString('')).toBe(4338908027751811); + expect(hashString('hello')).toBe(5625896200565286); + expect(hashString('TreeView')).toBe(4180486618474600); + expect(hashString('some-multiblock-file.vtm')).toBe(2654834946646323); + }); + + test('returns a safe integer in the configured ID range', () => { + const values = [ + hashString('a'), + hashString('b'), + hashString('a longer value'), + hashString('Unicode: ไฝ ๅฅฝ'), + hashString('๐Ÿš€'), + ]; + + values.forEach((value) => { + expect(Number.isSafeInteger(value)).toBe(true); + expect(value).toBeGreaterThanOrEqual(1_000_000_000_000_000); + expect(value).toBeLessThanOrEqual(Number.MAX_SAFE_INTEGER); + }); + }); + + test('normally produces different hashes for different strings', () => { + const values = [ + hashString('one'), + hashString('two'), + hashString('three'), + hashString('four'), + ]; + + expect(new Set(values).size).toBe(values.length); + }); +}); + +describe('randomInt', () => { + test('returns the minimum when Math.random returns zero', () => { + jest.spyOn(Math, 'random').mockReturnValue(0); + + expect(randomInt(10, 20)).toBe(10); + }); + + test('can return the maximum value', () => { + jest.spyOn(Math, 'random').mockReturnValue(0.95); + + expect(randomInt(10, 20)).toBe(20); + }); + + test('returns an integer within a custom range', () => { + jest.spyOn(Math, 'random').mockReturnValue(0.5); + + const result = randomInt(10, 20); + + expect(result).toBe(15); + expect(Number.isInteger(result)).toBe(true); + expect(result).toBeGreaterThanOrEqual(10); + expect(result).toBeLessThanOrEqual(20); + }); + + test('uses the documented default minimum', () => { + jest.spyOn(Math, 'random').mockReturnValue(0); + + expect(randomInt()).toBe(1_000_000_000_000_000); + }); +}); + +describe('randomId', () => { + test('returns an underscore followed by a random integer', () => { + jest.spyOn(Math, 'random').mockReturnValue(0); + + expect(randomId()).toBe('_1000000000000000'); + }); +}); + +describe('roundFloat', () => { + test.each([ + [1.005, 2, 1.01], + [123.456, 2, 123.46], + [1.2345, 3, 1.235], + [-1.005, 2, -1.01], + [123.4, 0, 123], + ])('rounds %p to %p places', (input, places, expected) => { + expect(roundFloat(input, places)).toBe(expected); + }); + + test('rounds to an integer when places is omitted', () => { + expect(roundFloat(123.5)).toBe(124); + }); +}); + +describe('setScale', () => { + test('adds a scale transform to an element', () => { + const element = document.createElement('div'); + element.style.transform = 'rotate(45deg)'; + + setScale(element, 2); + + expect(element.style.transform).toContain('scale(2,2)'); + expect(element.style.transform).toContain('rotate(45deg)'); + }); + + test('replaces an existing scale without removing other transforms', () => { + const element = document.createElement('div'); + element.style.transform = 'translateX(10px) scale(2) rotate(45deg)'; + + setScale(element, 3); + + expect(element.style.transform).toContain('scale(3,3)'); + expect(element.style.transform).not.toContain('scale(2)'); + expect(element.style.transform).toContain('translateX(10px)'); + expect(element.style.transform).toContain('rotate(45deg)'); + + const scaleMatches = element.style.transform.match(/scale\(/gi); + expect(scaleMatches).toHaveLength(1); + }); + + test('does not accumulate scale transforms across calls', () => { + const element = document.createElement('div'); + element.style.transform = 'rotate(20deg)'; + + setScale(element, 2); + setScale(element, 4); + + expect(element.style.transform).toContain('scale(4,4)'); + expect(element.style.transform).not.toContain('scale(2,2)'); + + const scaleMatches = element.style.transform.match(/scale\(/gi); + expect(scaleMatches).toHaveLength(1); + }); + + test('removes a none transform', () => { + const element = document.createElement('div'); + element.style.transform = 'none'; + + setScale(element, 1.5); + + expect(element.style.transform.trim()).toBe('scale(1.5,1.5)'); + }); +}); + +describe('formatNumber', () => { + test.each([ + [1234567.891, 2, false, '1,234,567.89'], + [1234, 2, true, '1,234.00'], + [1234.5, 2, true, '1,234.50'], + [0.5, 3, true, '0.500'], + [1234.6, 0, true, '1,235'], + [1234.567, -1, true, '1,234.567'], + [-1234567.89, -1, false, '-1,234,567.89'], + ])('formats %p with round=%p and pad=%p', (num, round, pad, expected) => { + expect(formatNumber(num, round, pad)).toBe(expected); + }); + + test('does not add decimal padding when pad is false', () => { + expect(formatNumber(1234.5, 3, false)).toBe('1,234.5'); + }); +}); + +describe('escapeHtml', () => { + test('escapes HTML special characters', () => { + expect(escapeHtml(`&<>"'`)).toBe('&<>"''); + }); + + test('escapes ampersands before other characters', () => { + expect(escapeHtml('&')).toBe('&amp;'); + }); + + test.each([ + [null, ''], + [undefined, ''], + ['', ''], + ])('converts %p to an empty string', (input, expected) => { + expect(escapeHtml(input)).toBe(expected); + }); + + test('leaves ordinary text unchanged', () => { + expect(escapeHtml('TreeView node 123')).toBe('TreeView node 123'); + }); +}); + +describe('completeAssign', () => { + test('returns the original target', () => { + const target = {}; + const result = completeAssign(target, false, { value: 1 }); + + expect(result).toBe(target); + expect(target.value).toBe(1); + }); + + test('copies a getter without invoking it', () => { + const getter = jest.fn().mockReturnValue(42); + const source = {}; + + Object.defineProperty(source, 'value', { + get: getter, + enumerable: true, + configurable: true, + }); + + const target = completeAssign({}, false, source); + + expect(getter).not.toHaveBeenCalled(); + + const descriptor = Object.getOwnPropertyDescriptor(target, 'value'); + + expect(descriptor.get).toBe(getter); + expect(descriptor.enumerable).toBe(true); + expect(descriptor.configurable).toBe(true); + + expect(target.value).toBe(42); + expect(getter).toHaveBeenCalledTimes(1); + }); + + test('copies non-enumerable and symbol properties', () => { + const symbol = Symbol('test'); + const source = {}; + + Object.defineProperty(source, 'hidden', { + value: 'hidden value', + enumerable: false, + writable: true, + configurable: true, + }); + + Object.defineProperty(source, symbol, { + value: 'symbol value', + enumerable: false, + writable: true, + configurable: true, + }); + + const target = completeAssign({}, false, source); + + expect(target.hidden).toBe('hidden value'); + expect(target[symbol]).toBe('symbol value'); + + expect(Object.getOwnPropertyDescriptor(target, 'hidden').enumerable).toBe(false); + }); + + test('makes copied data properties non-writable when seal is true', () => { + const source = {}; + + Object.defineProperty(source, 'value', { + value: 10, + enumerable: true, + writable: true, + configurable: true, + }); + + const target = completeAssign({}, true, source); + const descriptor = Object.getOwnPropertyDescriptor(target, 'value'); + + expect(descriptor.value).toBe(10); + expect(descriptor.writable).toBe(false); + expect(descriptor.configurable).toBe(false); + }); + + test('makes copied accessor properties non-configurable when sealed', () => { + const getter = jest.fn().mockReturnValue(10); + const source = {}; + + Object.defineProperty(source, 'value', { + get: getter, + enumerable: true, + configurable: true, + }); + + const target = completeAssign({}, true, source); + const descriptor = Object.getOwnPropertyDescriptor(target, 'value'); + + expect(descriptor.get).toBe(getter); + expect(descriptor.configurable).toBe(false); + }); + + test('throws when target is null or undefined', () => { + expect(() => completeAssign(null, false, {})).toThrow( + new TypeError('target cannot be null') + ); + + expect(() => completeAssign(undefined, false, {})).toThrow( + new TypeError('target cannot be null') + ); + }); + + test.each([null, undefined, 0, 1, 'true', {}])( + 'throws when seal is not a boolean: %p', + (seal) => { + expect(() => completeAssign({}, seal, {})).toThrow(TypeError); + } + ); + + test('throws when source is null or undefined', () => { + expect(() => completeAssign({}, false, null)).toThrow( + new TypeError('source cannot be null') + ); + + expect(() => completeAssign({}, false, undefined)).toThrow( + new TypeError('source cannot be null') + ); + }); +}); + +describe('lazy', () => { + test('does not call the factory until value is accessed', () => { + const factory = jest.fn().mockReturnValue('result'); + const shell = lazy(factory); + + expect(factory).not.toHaveBeenCalled(); + + expect(shell.value).toBe('result'); + expect(factory).toHaveBeenCalledTimes(1); + }); + + test('calls the factory only once and caches its result', () => { + const result = {}; + const factory = jest.fn().mockReturnValue(result); + const shell = lazy(factory); + + expect(shell.value).toBe(result); + expect(shell.value).toBe(result); + expect(shell.value).toBe(result); + expect(factory).toHaveBeenCalledTimes(1); + }); + + test('can cache null and undefined values', () => { + const nullFactory = jest.fn().mockReturnValue(null); + const undefinedFactory = jest.fn().mockReturnValue(undefined); + + const nullShell = lazy(nullFactory); + const undefinedShell = lazy(undefinedFactory); + + expect(nullShell.value).toBeNull(); + expect(nullShell.value).toBeNull(); + expect(nullFactory).toHaveBeenCalledTimes(1); + + expect(undefinedShell.value).toBeUndefined(); + expect(undefinedShell.value).toBeUndefined(); + expect(undefinedFactory).toHaveBeenCalledTimes(1); + }); + + test('throws when assigning to value', () => { + const shell = lazy(() => 'result'); + + expect(() => { + shell.value = 'replacement'; + }).toThrow('the value property is readonly'); + }); + + test('prevents accessing the lazy value from its factory', () => { + let shell; + shell = lazy(() => shell.value); + + expect(() => shell.value).toThrow('cannot access a lazy value from within its factory'); + }); + + test('retries the factory after the factory throws', () => { + const factory = jest + .fn() + .mockImplementationOnce(() => { + throw new Error('factory failed'); + }) + .mockReturnValue('success'); + + const shell = lazy(factory); + + expect(() => shell.value).toThrow('factory failed'); + expect(shell.value).toBe('success'); + expect(shell.value).toBe('success'); + expect(factory).toHaveBeenCalledTimes(2); + }); +}); + +describe('getPromiseResolver', () => { + test('provides a promise and external resolver', async () => { + const result = getPromiseResolver(); + + expect(result.promise).toBeInstanceOf(Promise); + expect(typeof result.resolver).toBe('function'); + expect(result.isSet()).toBe(false); + + result.resolver('resolved value'); + + expect(result.isSet()).toBe(true); + await expect(result.promise).resolves.toBe('resolved value'); + }); + + test('the promise retains the first resolved value', async () => { + const result = getPromiseResolver(); + + result.resolver('first'); + result.resolver('second'); + + await expect(result.promise).resolves.toBe('first'); + expect(result.isSet()).toBe(true); + }); +}); + +describe('getCookie', () => { + beforeEach(() => { + clearCookies(); + }); + + afterEach(() => { + clearCookies(); + }); + + test('returns the value of an existing cookie', () => { + document.cookie = 'testCookie=testValue; path=/'; + + expect(getCookie('testCookie')).toBe('testValue'); + }); + + test('finds a cookie between other cookies', () => { + document.cookie = 'first=one; path=/'; + document.cookie = 'target=two; path=/'; + document.cookie = 'last=three; path=/'; + + expect(getCookie('target')).toBe('two'); + }); + + test('encodes the requested cookie key', () => { + document.cookie = 'display%20name=TreeView; path=/'; + + expect(getCookie('display name')).toBe('TreeView'); + }); + + test('returns the stored cookie value without decoding it', () => { + document.cookie = 'encodedValue=hello%20world%21; path=/'; + + expect(getCookie('encodedValue')).toBe('hello%20world%21'); + }); +}); + +describe('isNumeric', () => { + test.each([0, 1, -1, 1.5, -1.5])('returns true for the number %p', (value) => { + expect(isNumeric(value)).toBe(true); + }); + + test.each(['0', '123', '-123', '1.5', '-1.5', '.5', '-.5', '1,5', '-1,5'])( + 'returns true for the numeric string %p', + (value) => { + expect(isNumeric(value)).toBe(true); + } + ); + + test.each([ + '', + ' ', + 'abc', + '1e3', + '0x10', + '1.', + '.', + '+1', + '--1', + '1,2,3', + null, + undefined, + true, + false, + {}, + [], + ])('returns false for the non-numeric value %p', (value) => { + expect(isNumeric(value)).toBe(false); + }); +}); + +describe('tryParseFloat', () => { + test.each([ + ['12', 12], + ['12.5', 12.5], + ['-0.25', -0.25], + ['.75', 0.75], + ['12px', 12], + ['', null], + [' ', null], + ['not a number', null], + ])('parses string %p as %p', (input, expected) => { + expect(tryParseFloat(input)).toBe(expected); + }); + + test('returns number inputs unchanged', () => { + expect(tryParseFloat(12.5)).toBe(12.5); + expect(tryParseFloat(-10)).toBe(-10); + expect(tryParseFloat(Number.POSITIVE_INFINITY)).toBe(Number.POSITIVE_INFINITY); + expect(tryParseFloat(Number.NaN)).toBeNaN(); + }); + + test('returns null and undefined unchanged', () => { + expect(tryParseFloat(null)).toBeNull(); + expect(tryParseFloat(undefined)).toBeUndefined(); + }); + + test.each([true, false, {}, [], () => {}])('throws for unsupported input %p', (input) => { + expect(() => tryParseFloat(input)).toThrow( + 'input must be a string, number, null, or undefined' + ); + }); +}); diff --git a/src/ansys/visor/visor-client/src/jest-tests/PrivateConstructor.test.js b/src/ansys/visor/visor-client/src/jest-tests/PrivateConstructor.test.js new file mode 100644 index 00000000..f2c97646 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/PrivateConstructor.test.js @@ -0,0 +1,21 @@ +import PrivateConstructor from '../utils/PrivateConstructor.js'; + +describe('PrivateConstructor', () => { + test('exports a class', () => { + expect(typeof PrivateConstructor).toBe('function'); + }); + + test('throws when instantiated directly', () => { + expect(() => new PrivateConstructor()).toThrow('constructor is private'); + }); + + test('throws an Error instance when instantiated', () => { + expect(() => new PrivateConstructor()).toThrow(Error); + }); + + test('prevents subclasses from being instantiated', () => { + class DerivedClass extends PrivateConstructor {} + + expect(() => new DerivedClass()).toThrow('constructor is private'); + }); +}); diff --git a/src/ansys/visor/visor-client/src/jest-tests/SchemaVersion.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/SchemaVersion.test.tsx new file mode 100644 index 00000000..8a4596a7 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/SchemaVersion.test.tsx @@ -0,0 +1,72 @@ +import VisorSceneDetails, { + SchemaVersionMismatchError, + SCENE_DETAILS_SCHEMA_VERSION, +} from '../state/appstate/VisorSceneDetails.tsx'; +import { parseRendererAnnotation, requireWasmAnnotation } from '../renderer/RendererAnnotation'; + +describe('SchemaVersion', () => { + test('mismatched schemaVersion throws SchemaVersionMismatchError before any sub-parse, carrying both versions', () => { + const payloadVersion = SCENE_DETAILS_SCHEMA_VERSION + 1; + const badPayload = { + schemaVersion: payloadVersion, + // Deliberately malformed: if the version check ran after any + // sub-parse, this would throw a plain TypeError first instead. + appState: undefined, + vtkInfo: undefined, + }; + let thrown: unknown = null; + try { + new VisorSceneDetails(badPayload); + } catch (err) { + thrown = err; + } + expect(thrown).toBeInstanceOf(SchemaVersionMismatchError); + const error = thrown as SchemaVersionMismatchError; + expect(error.payloadVersion).toBe(payloadVersion); + expect(error.expectedVersion).toBe(SCENE_DETAILS_SCHEMA_VERSION); + }); + + test('parseRendererAnnotation returns null for a null or undefined annotation', () => { + expect(parseRendererAnnotation(null)).toBeNull(); + expect(parseRendererAnnotation(undefined)).toBeNull(); + }); + + test('parseRendererAnnotation survives every field of a fully populated wasm annotation, by name and value', () => { + const wireAnnotation = { + rendererKind: 'wasm', + nodes: { + '5': { actorId: 10, propertyId: 11, mapperId: 12 }, + }, + widgets: { + orientationWidgetId: 1, + crossSectionPlaneId: 2, + crossSectionPlaneWidgetId: 3, + crossSectionPlaneRepresentationId: 4, + boundingBoxAlgorithmId: 5, + boundingBoxOutlineActorId: 6, + boundingBoxAxesActorId: 7, + }, + }; + const parsed = requireWasmAnnotation(parseRendererAnnotation(wireAnnotation)); + + expect(parsed.rendererKind).toBe('wasm'); + + // The three node handle fields. + expect(parsed.nodes['5'].actorId).toBe(10); + expect(parsed.nodes['5'].propertyId).toBe(11); + expect(parsed.nodes['5'].mapperId).toBe(12); + + // All seven widget fields. + expect(parsed.widgets.orientationWidgetId).toBe(1); + expect(parsed.widgets.crossSectionPlaneId).toBe(2); + expect(parsed.widgets.crossSectionPlaneWidgetId).toBe(3); + expect(parsed.widgets.crossSectionPlaneRepresentationId).toBe(4); + expect(parsed.widgets.boundingBoxAlgorithmId).toBe(5); + expect(parsed.widgets.boundingBoxOutlineActorId).toBe(6); + expect(parsed.widgets.boundingBoxAxesActorId).toBe(7); + }); + + test('requireWasmAnnotation throws when the annotation is absent (null)', () => { + expect(() => requireWasmAnnotation(null)).toThrow(TypeError); + }); +}); diff --git a/src/ansys/visor/visor-client/src/jest-tests/Tooltip.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/Tooltip.test.tsx new file mode 100644 index 00000000..e7865abe --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/Tooltip.test.tsx @@ -0,0 +1,362 @@ +/** @jest-environment jsdom */ + +import { fireEvent } from '@testing-library/react'; + +let makeTooltip: (target: HTMLElement, text: string) => () => void; + +beforeAll(() => { + // Tooltip.tsx calls crypto.randomUUID() during module initialization. + // Older jsdom versions may not provide it. + if (globalThis.crypto == null) { + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: {}, + }); + } + + if (typeof globalThis.crypto.randomUUID !== 'function') { + Object.defineProperty(globalThis.crypto, 'randomUUID', { + configurable: true, + value: () => '00000000-0000-4000-8000-000000000000', + }); + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-require-imports + ({ makeTooltip } = require('../utils/Tooltip')); +}); + +afterEach(() => { + document.body.innerHTML = ''; + jest.restoreAllMocks(); +}); + +describe('makeTooltip', () => { + test('creates a tooltip and appends it to the document body', () => { + const target = createTarget(); + + makeTooltip(target, 'Example tooltip'); + + const tip = getTooltip(); + + expect(tip.parentElement).toBe(document.body); + expect(tip.textContent).toBe('Example tooltip'); + expect(tip.classList.contains('visor-tooltip')).toBe(true); + expect(tip.classList.contains('theme-background-1')).toBe(true); + }); + + test('applies the expected inline styles', () => { + const target = createTarget(); + + makeTooltip(target, 'Example tooltip'); + + const style = getTooltip().style; + + expect(style.position).toBe('absolute'); + expect(style.padding).toBe('6px 8px'); + expect(style.borderWidth).toBe('1px'); + expect(style.borderRadius).toBe('4px'); + expect(style.fontSize).toBe('12px'); + expect(style.pointerEvents).toBe('none'); + expect(style.transform).toBe('translateY(-8px)'); + expect(style.transition).toBe('opacity 0.12s ease, transform 0.12s ease'); + expect(style.opacity).toBe('0'); + expect(style.zIndex).toBe('10000'); + expect(style.whiteSpace).toBe('nowrap'); + }); + + test('uses textContent rather than interpreting text as HTML', () => { + const target = createTarget(); + + makeTooltip(target, 'Tooltip'); + + const tip = getTooltip(); + + expect(tip.textContent).toBe('Tooltip'); + expect(tip.querySelector('strong')).toBeNull(); + }); + + test('shows and positions the tooltip on mouseenter', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 100, + top: 50, + width: 60, + height: 30, + }); + + makeTooltip(target, 'Example tooltip'); + + const tip = getTooltip(); + setTooltipDimensions(tip, 40, 20); + + fireEvent.mouseEnter(target); + + // left = 100 + 60 / 2 - 40 / 2 = 110 + // top = 50 - 20 - 8 = 22 + expect(tip.style.left).toBe('110px'); + expect(tip.style.top).toBe('22px'); + expect(tip.style.opacity).toBe('1'); + expect(tip.style.transform).toBe('translateY(0px)'); + }); + + test('shows and positions the tooltip on focus', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 200, + top: 100, + width: 80, + height: 30, + }); + + makeTooltip(target, 'Example tooltip'); + + const tip = getTooltip(); + setTooltipDimensions(tip, 60, 24); + + fireEvent.focus(target); + + // left = 200 + 80 / 2 - 60 / 2 = 210 + // top = 100 - 24 - 8 = 68 + expect(tip.style.left).toBe('210px'); + expect(tip.style.top).toBe('68px'); + expect(tip.style.opacity).toBe('1'); + expect(tip.style.transform).toBe('translateY(0px)'); + }); + + test('rounds calculated positions to whole pixels', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 100.4, + top: 50.6, + width: 51, + height: 20, + }); + + makeTooltip(target, 'Example tooltip'); + + const tip = getTooltip(); + setTooltipDimensions(tip, 20, 10); + + fireEvent.mouseEnter(target); + + // Math.round(100.4 + 25.5 - 10) = 116 + // Math.round(50.6 - 10 - 8) = 33 + expect(tip.style.left).toBe('116px'); + expect(tip.style.top).toBe('33px'); + }); + + test('clamps the tooltip position to at least eight pixels', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 0, + top: 5, + width: 10, + height: 10, + }); + + makeTooltip(target, 'Example tooltip'); + + const tip = getTooltip(); + setTooltipDimensions(tip, 100, 30); + + fireEvent.mouseEnter(target); + + expect(tip.style.left).toBe('8px'); + expect(tip.style.top).toBe('8px'); + }); + + test('hides the tooltip on mouseleave', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 100, + top: 100, + width: 50, + height: 20, + }); + + makeTooltip(target, 'Example tooltip'); + + const tip = getTooltip(); + setTooltipDimensions(tip, 40, 20); + + fireEvent.mouseEnter(target); + + expect(tip.style.opacity).toBe('1'); + expect(tip.style.transform).toBe('translateY(0px)'); + + fireEvent.mouseLeave(target); + + expect(tip.style.opacity).toBe('0'); + expect(tip.style.transform).toBe('translateY(-8px)'); + }); + + test('hides the tooltip on blur', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 100, + top: 100, + width: 50, + height: 20, + }); + + makeTooltip(target, 'Example tooltip'); + + const tip = getTooltip(); + setTooltipDimensions(tip, 40, 20); + + fireEvent.focus(target); + + expect(tip.style.opacity).toBe('1'); + + fireEvent.blur(target); + + expect(tip.style.opacity).toBe('0'); + expect(tip.style.transform).toBe('translateY(-8px)'); + }); + + test('does not add a second tooltip to the same target', () => { + const target = createTarget(); + + makeTooltip(target, 'First tooltip'); + const secondCleanup = makeTooltip(target, 'Second tooltip'); + + const tooltips = document.querySelectorAll('.visor-tooltip'); + + expect(tooltips).toHaveLength(1); + expect(tooltips[0].textContent).toBe('First tooltip'); + expect(typeof secondCleanup).toBe('function'); + expect(secondCleanup()).toBeUndefined(); + }); + + test('allows different targets to have separate tooltips', () => { + const firstTarget = createTarget(); + const secondTarget = createTarget(); + + makeTooltip(firstTarget, 'First tooltip'); + makeTooltip(secondTarget, 'Second tooltip'); + + const tooltips = document.querySelectorAll('.visor-tooltip'); + + expect(tooltips).toHaveLength(2); + expect(tooltips[0].textContent).toBe('First tooltip'); + expect(tooltips[1].textContent).toBe('Second tooltip'); + }); + + test('cleanup removes the target event listeners', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 100, + top: 100, + width: 50, + height: 20, + }); + + const cleanup = makeTooltip(target, 'Example tooltip'); + const tip = getTooltip(); + + setTooltipDimensions(tip, 40, 20); + + cleanup(); + fireEvent.mouseEnter(target); + fireEvent.focus(target); + + expect(tip.style.opacity).toBe('0'); + expect(tip.style.transform).toBe('translateY(-8px)'); + expect(tip.style.left).toBe(''); + expect(tip.style.top).toBe(''); + }); + + test('cleanup stops mouseleave and blur from changing the tooltip', () => { + const target = createTarget(); + + setTargetBounds(target, { + left: 100, + top: 100, + width: 50, + height: 20, + }); + + const cleanup = makeTooltip(target, 'Example tooltip'); + const tip = getTooltip(); + + setTooltipDimensions(tip, 40, 20); + fireEvent.mouseEnter(target); + + expect(tip.style.opacity).toBe('1'); + + cleanup(); + + fireEvent.mouseLeave(target); + fireEvent.blur(target); + + expect(tip.style.opacity).toBe('1'); + expect(tip.style.transform).toBe('translateY(0px)'); + }); + + test('cleanup leaves the tooltip element in the document', () => { + const target = createTarget(); + const cleanup = makeTooltip(target, 'Example tooltip'); + const tip = getTooltip(); + + cleanup(); + + expect(document.body.contains(tip)).toBe(true); + }); +}); + +function createTarget(): HTMLButtonElement { + const target = document.createElement('button'); + document.body.appendChild(target); + return target; +} + +function getTooltip(): HTMLDivElement { + const tip = document.querySelector('.visor-tooltip'); + + expect(tip).not.toBeNull(); + + return tip!; +} + +function setTooltipDimensions(tip: HTMLElement, width: number, height: number): void { + Object.defineProperty(tip, 'offsetWidth', { + configurable: true, + value: width, + }); + + Object.defineProperty(tip, 'offsetHeight', { + configurable: true, + value: height, + }); +} + +function setTargetBounds( + target: HTMLElement, + bounds: { + left: number; + top: number; + width: number; + height: number; + } +): void { + jest.spyOn(target, 'getBoundingClientRect').mockReturnValue({ + x: bounds.left, + y: bounds.top, + left: bounds.left, + top: bounds.top, + width: bounds.width, + height: bounds.height, + right: bounds.left + bounds.width, + bottom: bounds.top + bounds.height, + toJSON: () => ({}), + } as DOMRect); +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/TreeView.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/TreeView.test.tsx new file mode 100644 index 00000000..b4762fac --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/TreeView.test.tsx @@ -0,0 +1,196 @@ +import { render, fireEvent } from '@testing-library/react'; +import { TreeView, TreeViewUtil } from '../treeview/TreeView'; +import { + CreateVisorSceneGraph, + VisorSceneNodeExtended, + VisorSceneNodeSimple, +} from '../state/VisorSceneGraph.tsx'; + +describe('TreeView', () => { + it('renders without a scene graph', () => { + render(); + }); + it('renders with a simple scene graph', () => { + render(); + }); + it('renders with a complex scene graph', () => { + render(); + }); + test('component util is not null for a simple scene graph', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + expect(util).toBeTruthy(); + }); + test('component util contains the correct node count for a simple scene graph', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + expect(util.nodeCount).toBe(2); + }); + test('component util is not null for a complex scene graph', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + expect(util).toBeTruthy(); + }); + test('component util contains the correct node count for a complex scene graph', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + expect(util.nodeCount).toBe(8); + }); + test('root node row is the first element in the rows array', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + expect(util.rows[0].node.nodeType).toBe('root'); + }); + test('root node row expand button works', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + const rootRow = util.rows[0]; + fireEvent.click(rootRow.expandButton); + expect(rootRow.expanded).toBe(true); + }); + test('root node row collapse button works', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + const rootRow = util.rows[0]; + fireEvent.click(rootRow.collapseButton); + expect(rootRow.expanded).toBe(false); + }); + test('root node row show button works', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + const rootRow = util.rows[0]; + fireEvent.click(rootRow.showButton); + expect(rootRow.visible).toBe(true); + }); + test('root node row hide button works', () => { + let util: TreeViewUtil = null!; + render( (util = u)} />); + const rootRow = util.rows[0]; + fireEvent.click(rootRow.hideButton); + expect(rootRow.visible).toBe(false); + }); +}); + +function getSmallSceneGraph(): VisorSceneNodeExtended { + return CreateVisorSceneGraph({ + id: 0, + dataArrays: [], + name: '', + isGroupNode: true, + isActorNode: false, + nodeType: 'root', + diffuseColor: [1, 1, 1], + bounds: [], + children: [ + { + id: 1, + dataArrays: [], + name: 'some-polydata-file.vtp', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkUnstructuredGrid', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + ], + }); +} + +function getLargeSceneGraph(): VisorSceneNodeExtended { + return CreateVisorSceneGraph(getSceneNodeSimple()); + + function getSceneNodeSimple(): VisorSceneNodeSimple { + return { + id: 0, + dataArrays: [], + name: '', + isGroupNode: true, + isActorNode: false, + nodeType: 'root', + diffuseColor: [1, 1, 1], + bounds: [], + children: [ + { + id: 1, + dataArrays: [], + name: 'some-multiblock-file.vtm', + isGroupNode: true, + isActorNode: false, + nodeType: 'vtkMultiBlockDataSet', + diffuseColor: [1, 1, 1], + bounds: [], + children: [ + { + id: 2, + dataArrays: [], + name: 'PolyData Container', + isGroupNode: true, + isActorNode: false, + nodeType: 'vtkMultiBlockDataSet', + diffuseColor: [1, 1, 1], + bounds: [], + children: [ + { + id: 3, + dataArrays: [], + name: 'mesh A', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkPolyData', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + { + id: 4, + dataArrays: [], + name: 'mesh B', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkPolyData', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + ], + }, + { + id: 5, + dataArrays: [], + name: 'mesh C', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkUnstructuredGrid', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + { + id: 6, + dataArrays: [], + name: 'mesh D', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkUnstructuredGrid', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + { + id: 7, + dataArrays: [], + name: 'mesh E', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkUnstructuredGrid', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + ], + }, + ], + }; + } +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/VisorColor.test.js b/src/ansys/visor/visor-client/src/jest-tests/VisorColor.test.js new file mode 100644 index 00000000..40dc5b15 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/VisorColor.test.js @@ -0,0 +1,286 @@ +/** @jest-environment jsdom */ + +import VisorColor from '../utils/VisorColor.tsx'; + +describe('VisorColor', () => { + describe('constructor', () => { + test('creates a white color by default', () => { + const color = new VisorColor(); + + expect(color.hex).toBe('#ffffff'); + expect(color.rgb).toEqual([255, 255, 255]); + expect(color.rgbNormalized).toEqual([1, 1, 1]); + }); + + test('creates frozen RGB arrays', () => { + const color = new VisorColor(); + + expect(Object.isFrozen(color.rgb)).toBe(true); + expect(Object.isFrozen(color.rgbNormalized)).toBe(true); + }); + }); + + describe('setHex', () => { + test('sets a six-digit hex color', () => { + const color = new VisorColor(); + + color.setHex('#336699'); + + expect(color.hex).toBe('#336699'); + expect(color.rgb).toEqual([51, 102, 153]); + expect(color.rgbNormalized[0]).toBeCloseTo(51 / 255); + expect(color.rgbNormalized[1]).toBeCloseTo(102 / 255); + expect(color.rgbNormalized[2]).toBeCloseTo(153 / 255); + }); + + test('accepts a six-digit hex color without a hash', () => { + const color = new VisorColor(); + + color.setHex('ff8000'); + + expect(color.hex).toBe('ff8000'); + expect(color.rgb).toEqual([255, 128, 0]); + expect(color.rgbNormalized).toEqual([1, 128 / 255, 0]); + }); + + test('expands a three-digit shorthand hex color', () => { + const color = new VisorColor(); + + color.setHex('#03F'); + + expect(color.hex).toBe('#03F'); + expect(color.rgb).toEqual([0, 51, 255]); + expect(color.rgbNormalized).toEqual([0, 51 / 255, 1]); + }); + + test('accepts shorthand hex without a hash', () => { + const color = new VisorColor(); + + color.setHex('abc'); + + expect(color.hex).toBe('abc'); + expect(color.rgb).toEqual([170, 187, 204]); + }); + + test('accepts uppercase hex characters', () => { + const color = new VisorColor(); + + color.setHex('#A1B2C3'); + + expect(color.hex).toBe('#A1B2C3'); + expect(color.rgb).toEqual([161, 178, 195]); + }); + + test.each([ + '', + '#', + '#12', + '#1234', + '#12345', + '#1234567', + '#gggggg', + 'not-a-color', + 'rgb(1, 2, 3)', + ' #ffffff', + '#ffffff ', + ])('throws for invalid hex input %p', (hex) => { + const color = new VisorColor(); + + expect(() => color.setHex(hex)).toThrow(`invalid hex string: ${hex}`); + }); + + test('does not change the color when given invalid input', () => { + const color = new VisorColor(); + + color.setHex('#123456'); + + expect(() => color.setHex('invalid')).toThrow(); + + expect(color.hex).toBe('#123456'); + expect(color.rgb).toEqual([18, 52, 86]); + expect(color.rgbNormalized).toEqual([18 / 255, 52 / 255, 86 / 255]); + }); + + test('replaces the frozen RGB arrays', () => { + const color = new VisorColor(); + const originalRgb = color.rgb; + const originalNormalized = color.rgbNormalized; + + color.setHex('#000000'); + + expect(color.rgb).not.toBe(originalRgb); + expect(color.rgbNormalized).not.toBe(originalNormalized); + expect(Object.isFrozen(color.rgb)).toBe(true); + expect(Object.isFrozen(color.rgbNormalized)).toBe(true); + + expect(originalRgb).toEqual([255, 255, 255]); + expect(originalNormalized).toEqual([1, 1, 1]); + }); + + test('returns the color instance to support chaining', () => { + const color = new VisorColor(); + + expect(color.setHex('#123456')).toBe(color); + }); + }); + + describe('setRgb with normalized values', () => { + test('uses normalized values by default', () => { + const color = new VisorColor(); + + color.setRgb(0.5, 0.25, 1); + + expect(color.hex).toBe('#7f3fff'); + expect(color.rgb).toEqual([127, 63, 255]); + expect(color.rgbNormalized).toEqual([0.5, 0.25, 1]); + }); + + test('converts black correctly', () => { + const color = new VisorColor(); + + color.setRgb(0, 0, 0); + + expect(color.hex).toBe('#000000'); + expect(color.rgb).toEqual([0, 0, 0]); + expect(color.rgbNormalized).toEqual([0, 0, 0]); + }); + + test('converts white correctly', () => { + const color = new VisorColor(); + + color.setRgb(1, 1, 1); + + expect(color.hex).toBe('#ffffff'); + expect(color.rgb).toEqual([255, 255, 255]); + expect(color.rgbNormalized).toEqual([1, 1, 1]); + }); + + test('floors converted byte values', () => { + const color = new VisorColor(); + + color.setRgb(0.999, 0.501, 0.001); + + expect(color.rgb).toEqual([ + Math.floor(0.999 * 255), + Math.floor(0.501 * 255), + Math.floor(0.001 * 255), + ]); + expect(color.hex).toBe('#fe7f00'); + }); + + test('freezes both RGB arrays', () => { + const color = new VisorColor(); + + color.setRgb(0.1, 0.2, 0.3); + + expect(Object.isFrozen(color.rgb)).toBe(true); + expect(Object.isFrozen(color.rgbNormalized)).toBe(true); + }); + }); + + describe('setRgb with byte values', () => { + test('sets a color using byte RGB values', () => { + const color = new VisorColor(); + + color.setRgb(12, 34, 56, false); + + expect(color.hex).toBe('#0c2238'); + expect(color.rgb).toEqual([12, 34, 56]); + expect(color.rgbNormalized).toEqual([12 / 255, 34 / 255, 56 / 255]); + }); + + test('pads one-digit hexadecimal components with zeroes', () => { + const color = new VisorColor(); + + color.setRgb(1, 2, 3, false); + + expect(color.hex).toBe('#010203'); + }); + + test('accepts the minimum byte values', () => { + const color = new VisorColor(); + + color.setRgb(0, 0, 0, false); + + expect(color.hex).toBe('#000000'); + }); + + test('accepts the maximum byte values', () => { + const color = new VisorColor(); + + color.setRgb(255, 255, 255, false); + + expect(color.hex).toBe('#ffffff'); + }); + + test('returns the color instance to support chaining', () => { + const color = new VisorColor(); + + expect(color.setRgb(1, 2, 3, false)).toBe(color); + }); + }); + + describe('setRgb validation', () => { + test.each([ + [-1, 0, 0, 'r must be between 0 and 255'], + [256, 0, 0, 'r must be between 0 and 255'], + [0, -1, 0, 'g must be between 0 and 255'], + [0, 256, 0, 'g must be between 0 and 255'], + [0, 0, -1, 'b must be between 0 and 255'], + [0, 0, 256, 'b must be between 0 and 255'], + ])('rejects byte RGB values (%p, %p, %p)', (r, g, b, expectedMessage) => { + const color = new VisorColor(); + + expect(() => color.setRgb(r, g, b, false)).toThrow(expectedMessage); + }); + + test.each([ + [-0.01, 0, 0, 'r must be between 0 and 255'], + [1.01, 0, 0, 'r must be between 0 and 255'], + [0, -0.01, 0, 'g must be between 0 and 255'], + [0, 1.01, 0, 'g must be between 0 and 255'], + [0, 0, -0.01, 'b must be between 0 and 255'], + [0, 0, 1.01, 'b must be between 0 and 255'], + ])('rejects normalized RGB values (%p, %p, %p)', (r, g, b, expectedMessage) => { + const color = new VisorColor(); + + expect(() => color.setRgb(r, g, b)).toThrow(expectedMessage); + }); + + test('does not change the color after invalid RGB input', () => { + const color = new VisorColor(); + + color.setRgb(10, 20, 30, false); + + expect(() => color.setRgb(256, 20, 30, false)).toThrow(); + + expect(color.hex).toBe('#0a141e'); + expect(color.rgb).toEqual([10, 20, 30]); + expect(color.rgbNormalized).toEqual([10 / 255, 20 / 255, 30 / 255]); + }); + }); + + describe('successive updates', () => { + test('can update from hex to RGB', () => { + const color = new VisorColor(); + + color.setHex('#ff0000'); + color.setRgb(0, 255, 0, false); + + expect(color.hex).toBe('#00ff00'); + expect(color.rgb).toEqual([0, 255, 0]); + expect(color.rgbNormalized).toEqual([0, 1, 0]); + }); + + test('can update from RGB to hex', () => { + const color = new VisorColor(); + + color.setRgb(0, 0, 255, false); + color.setHex('#ffff00'); + + expect(color.hex).toBe('#ffff00'); + expect(color.rgb).toEqual([255, 255, 0]); + expect(color.rgbNormalized).toEqual([1, 1, 0]); + }); + }); +}); diff --git a/src/ansys/visor/visor-client/src/jest-tests/VisorSpectrumManager.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/VisorSpectrumManager.test.tsx new file mode 100644 index 00000000..ecd68eca --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/VisorSpectrumManager.test.tsx @@ -0,0 +1,633 @@ +import VisorVtkDataArray from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; +import { getSpectrumManager, VisorSpectrumInfo } from '../state/VisorSpectrumManager.tsx'; + +describe('getSpectrumManager', () => { + describe('manager lifecycle', () => { + test('returns a frozen manager object', () => { + const manager = getSpectrumManager(); + + expect(Object.isFrozen(manager)).toBe(true); + expect(typeof manager.addDataArrayMetadata).toBe('function'); + expect(typeof manager.finishAddingDataArrayMetadata).toBe('function'); + }); + + test('throws when globalSpectrumCollection is read before finishing', () => { + const manager = getSpectrumManager(); + + expect(() => manager.globalSpectrumCollection).toThrow( + 'finishAddingDataArrayMetadata() has not been called yet' + ); + }); + + test('creates an empty global collection when no metadata was added', () => { + const manager = getSpectrumManager(); + + manager.finishAddingDataArrayMetadata(); + + expect(manager.globalSpectrumCollection.array).toEqual([]); + expect(manager.globalSpectrumCollection.getSpectrum(null)).toBeNull(); + expect(manager.globalSpectrumCollection.getSpectrum('missing')).toBeNull(); + }); + + test('throws when finishAddingDataArrayMetadata is called twice', () => { + const manager = getSpectrumManager(); + + manager.finishAddingDataArrayMetadata(); + + expect(() => { + manager.finishAddingDataArrayMetadata(); + }).toThrow('finishAddingDataArrayMetadata() has already been called'); + }); + + test('different managers have independent state', () => { + const firstManager = getSpectrumManager(); + const secondManager = getSpectrumManager(); + + firstManager.addDataArrayMetadata([createDataArray()]); + firstManager.finishAddingDataArrayMetadata(); + secondManager.finishAddingDataArrayMetadata(); + + expect(firstManager.globalSpectrumCollection.array).toHaveLength(1); + expect(secondManager.globalSpectrumCollection.array).toHaveLength(0); + }); + }); + + describe('addDataArrayMetadata', () => { + test('returns an empty frozen collection for an empty array', () => { + const manager = getSpectrumManager(); + + const collection = manager.addDataArrayMetadata([]); + + expect(collection.array).toEqual([]); + expect(Object.isFrozen(collection)).toBe(true); + expect(Object.isFrozen(collection.array)).toBe(true); + }); + + test('creates scalar spectrum metadata', () => { + const manager = getSpectrumManager(); + + const collection = manager.addDataArrayMetadata([ + createDataArray({ + type: 'POINT', + name: 'temperature', + numComponents: 1, + magnitudeRange: [-20, 100], + ranges: [[-20, 100]], + }), + ]); + + const spectrum = collection.array[0]; + + expect(spectrum.id).toBe('POINT::temperature::1'); + expect(spectrum.type).toBe('POINT'); + expect(spectrum.name).toBe('temperature'); + expect(spectrum.shape).toBe('Scalar'); + expect(spectrum.fullName).toBe('POINT - temperature (Scalar)'); + expect(spectrum.numComponents).toBe(1); + expect(spectrum.componentOptions).toEqual([ + { + id: -1, + name: 'Magnitude', + }, + ]); + }); + + test('creates Vector2 component options', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + numComponents: 2, + ranges: [ + [-1, 1], + [-2, 2], + ], + }) + ); + + expect(spectrum.shape).toBe('Vector2'); + expect(spectrum.componentOptions).toEqual([ + { id: -1, name: 'Magnitude' }, + { id: 0, name: 'X' }, + { id: 1, name: 'Y' }, + ]); + }); + + test('creates Vector3 component options', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + numComponents: 3, + ranges: [ + [-1, 1], + [-2, 2], + [-3, 3], + ], + }) + ); + + expect(spectrum.shape).toBe('Vector3'); + expect(spectrum.componentOptions).toEqual([ + { id: -1, name: 'Magnitude' }, + { id: 0, name: 'X' }, + { id: 1, name: 'Y' }, + { id: 2, name: 'Z' }, + ]); + }); + + test('creates Vector4 component options', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + numComponents: 4, + ranges: [ + [-1, 1], + [-2, 2], + [-3, 3], + [-4, 4], + ], + }) + ); + + expect(spectrum.shape).toBe('Vector4'); + expect(spectrum.componentOptions).toEqual([ + { id: -1, name: 'Magnitude' }, + { id: 0, name: 'X' }, + { id: 1, name: 'Y' }, + { id: 2, name: 'Z' }, + { id: 3, name: 'W' }, + ]); + }); + + test('creates nine-component tensor labels', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + numComponents: 9, + ranges: Array.from({ length: 9 }, (_, i) => [-i, i]), + }) + ); + + expect(spectrum.componentOptions).toEqual([ + { id: -1, name: 'Magnitude' }, + { id: 0, name: 'XX' }, + { id: 1, name: 'XY' }, + { id: 2, name: 'XZ' }, + { id: 3, name: 'YX' }, + { id: 4, name: 'YY' }, + { id: 5, name: 'YZ' }, + { id: 6, name: 'ZX' }, + { id: 7, name: 'ZY' }, + { id: 8, name: 'ZZ' }, + ]); + }); + + test('throws for an unsupported component count', () => { + const manager = getSpectrumManager(); + + expect(() => { + manager.addDataArrayMetadata([ + createDataArray({ + numComponents: 5, + ranges: [ + [0, 1], + [0, 1], + [0, 1], + [0, 1], + [0, 1], + ], + }), + ]); + }).toThrow('Do we support label info for data arrays with 5 component(s)?'); + }); + + test('returns spectra in input order', () => { + const manager = getSpectrumManager(); + + const collection = manager.addDataArrayMetadata([ + createDataArray({ + name: 'first', + }), + createDataArray({ + name: 'second', + }), + ]); + + expect(collection.array.map((item) => item.name)).toEqual(['first', 'second']); + }); + + test('creates separate spectra for different names', () => { + const manager = getSpectrumManager(); + + const collection = manager.addDataArrayMetadata([ + createDataArray({ + name: 'temperature', + }), + createDataArray({ + name: 'pressure', + }), + ]); + + expect(collection.array).toHaveLength(2); + expect(collection.array[0]).not.toBe(collection.array[1]); + }); + + test('creates separate spectra for different types', () => { + const manager = getSpectrumManager(); + + const collection = manager.addDataArrayMetadata([ + createDataArray({ + type: 'POINT', + }), + createDataArray({ + type: 'CELL', + }), + ]); + + expect(collection.array).toHaveLength(2); + expect(collection.array[0].id).toBe('POINT::displacement::3'); + expect(collection.array[1].id).toBe('CELL::displacement::3'); + }); + + test('creates separate spectra for different component counts', () => { + const manager = getSpectrumManager(); + + const collection = manager.addDataArrayMetadata([ + createDataArray({ + numComponents: 2, + ranges: [ + [-1, 1], + [-2, 2], + ], + }), + createDataArray({ + numComponents: 3, + ranges: [ + [-1, 1], + [-2, 2], + [-3, 3], + ], + }), + ]); + + expect(collection.array).toHaveLength(2); + expect(collection.array[0].id).toBe('POINT::displacement::2'); + expect(collection.array[1].id).toBe('POINT::displacement::3'); + }); + }); + + describe('local spectrum collections', () => { + test('looks up a spectrum by ID', () => { + const manager = getSpectrumManager(); + const collection = manager.addDataArrayMetadata([createDataArray()]); + + const spectrum = collection.array[0]; + + expect(collection.getSpectrum(spectrum.id)).toBe(spectrum); + }); + + test('returns null for null and unknown IDs', () => { + const manager = getSpectrumManager(); + const collection = manager.addDataArrayMetadata([createDataArray()]); + + expect(collection.getSpectrum(null)).toBeNull(); + expect(collection.getSpectrum('unknown')).toBeNull(); + }); + + test('returns a frozen collection and array', () => { + const manager = getSpectrumManager(); + const collection = manager.addDataArrayMetadata([createDataArray()]); + + expect(Object.isFrozen(collection)).toBe(true); + expect(Object.isFrozen(collection.array)).toBe(true); + }); + + test('spectrum metadata objects are frozen', () => { + const spectrum = addSingleSpectrum(createDataArray()); + + expect(Object.isFrozen(spectrum)).toBe(true); + }); + }); + + describe('range information', () => { + test('returns the magnitude range for component -1', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + magnitudeRange: [0, 10], + }) + ); + + expect(spectrum.getRangeInfo(-1)).toEqual({ + defaultRange: [0, 10], + customRange: [0, 10], + }); + }); + + test('returns the range for an individual component', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + ranges: [ + [-1, 1], + [-2, 2], + [-3, 3], + ], + }) + ); + + expect(spectrum.getRangeInfo(0)).toEqual({ + defaultRange: [-1, 1], + customRange: [-1, 1], + }); + + expect(spectrum.getRangeInfo(2)).toEqual({ + defaultRange: [-3, 3], + customRange: [-3, 3], + }); + }); + + test('returns null for null, undefined, and out-of-range components', () => { + const spectrum = addSingleSpectrum(createDataArray()); + + expect(spectrum.getRangeInfo(null)).toBeNull(); + expect(spectrum.getRangeInfo(undefined)).toBeNull(); + expect(spectrum.getRangeInfo(-2)).toBeNull(); + expect(spectrum.getRangeInfo(3)).toBeNull(); + expect(spectrum.getRangeInfo(100)).toBeNull(); + }); + + test('returns cloned range arrays', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + magnitudeRange: [0, 10], + }) + ); + + const first = spectrum.getRangeInfo(-1)!; + first.defaultRange[0] = -999; + first.customRange[1] = 999; + + const second = spectrum.getRangeInfo(-1)!; + + expect(second.defaultRange).toEqual([0, 10]); + expect(second.customRange).toEqual([0, 10]); + expect(second.defaultRange).not.toBe(first.defaultRange); + expect(second.customRange).not.toBe(first.customRange); + }); + + test('setCustomRange changes only the custom range', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + ranges: [ + [-1, 1], + [-2, 2], + [-3, 3], + ], + }) + ); + + spectrum.setCustomRange(1, -20, 20); + + expect(spectrum.getRangeInfo(1)).toEqual({ + defaultRange: [-2, 2], + customRange: [-20, 20], + }); + }); + + test('setCustomRange can change the magnitude range', () => { + const spectrum = addSingleSpectrum( + createDataArray({ + magnitudeRange: [0, 10], + }) + ); + + spectrum.setCustomRange(-1, 2, 8); + + expect(spectrum.getRangeInfo(-1)).toEqual({ + defaultRange: [0, 10], + customRange: [2, 8], + }); + }); + + test('setCustomRange ignores invalid component IDs', () => { + const spectrum = addSingleSpectrum(createDataArray()); + + spectrum.setCustomRange(-2, -100, 100); + spectrum.setCustomRange(100, -100, 100); + + expect(spectrum.getRangeInfo(-1)).toEqual({ + defaultRange: [0, 10], + customRange: [0, 10], + }); + }); + }); + + describe('duplicate spectrum aggregation', () => { + test('reuses the same spectrum object for the same ID', () => { + const manager = getSpectrumManager(); + + const firstCollection = manager.addDataArrayMetadata([createDataArray()]); + + const secondCollection = manager.addDataArrayMetadata([createDataArray()]); + + expect(secondCollection.array[0]).toBe(firstCollection.array[0]); + }); + + test('expands default ranges using duplicate metadata', () => { + const manager = getSpectrumManager(); + + const firstSpectrum = manager.addDataArrayMetadata([ + createDataArray({ + magnitudeRange: [0, 10], + ranges: [ + [-1, 1], + [-2, 2], + [-3, 3], + ], + }), + ]).array[0]; + + manager.addDataArrayMetadata([ + createDataArray({ + magnitudeRange: [-5, 20], + ranges: [ + [-10, 0.5], + [-1, 15], + [-30, 30], + ], + }), + ]); + + expect(firstSpectrum.getRangeInfo(-1)).toEqual({ + defaultRange: [-5, 20], + customRange: [-5, 20], + }); + + expect(firstSpectrum.getRangeInfo(0)).toEqual({ + defaultRange: [-10, 1], + customRange: [-10, 1], + }); + + expect(firstSpectrum.getRangeInfo(1)).toEqual({ + defaultRange: [-2, 15], + customRange: [-2, 15], + }); + + expect(firstSpectrum.getRangeInfo(2)).toEqual({ + defaultRange: [-30, 30], + customRange: [-30, 30], + }); + }); + + test('resets custom ranges to the expanded defaults when duplicate metadata is added', () => { + const manager = getSpectrumManager(); + + const spectrum = manager.addDataArrayMetadata([ + createDataArray({ + magnitudeRange: [0, 10], + }), + ]).array[0]; + + spectrum.setCustomRange(-1, 2, 8); + + expect(spectrum.getRangeInfo(-1)?.customRange).toEqual([2, 8]); + + manager.addDataArrayMetadata([ + createDataArray({ + magnitudeRange: [-5, 20], + }), + ]); + + expect(spectrum.getRangeInfo(-1)).toEqual({ + defaultRange: [-5, 20], + customRange: [-5, 20], + }); + }); + + test('keeps existing bounds when duplicate ranges are narrower', () => { + const manager = getSpectrumManager(); + + const spectrum = manager.addDataArrayMetadata([ + createDataArray({ + magnitudeRange: [-10, 20], + }), + ]).array[0]; + + manager.addDataArrayMetadata([ + createDataArray({ + magnitudeRange: [-5, 10], + }), + ]); + + expect(spectrum.getRangeInfo(-1)).toEqual({ + defaultRange: [-10, 20], + customRange: [-10, 20], + }); + }); + }); + + describe('global spectrum collection', () => { + test('contains spectra added before finishing', () => { + const manager = getSpectrumManager(); + + manager.addDataArrayMetadata([ + createDataArray({ + name: 'temperature', + }), + createDataArray({ + name: 'pressure', + }), + ]); + + manager.finishAddingDataArrayMetadata(); + + const global = manager.globalSpectrumCollection; + + expect(global.array).toHaveLength(2); + expect(global.array.map((item) => item.name)).toEqual(['temperature', 'pressure']); + }); + + test('contains only one entry for duplicate spectrum IDs', () => { + const manager = getSpectrumManager(); + + manager.addDataArrayMetadata([ + createDataArray({ + name: 'temperature', + }), + ]); + + manager.addDataArrayMetadata([ + createDataArray({ + name: 'temperature', + }), + ]); + + manager.finishAddingDataArrayMetadata(); + + expect(manager.globalSpectrumCollection.array).toHaveLength(1); + }); + + test('returns the same spectrum object as a local collection', () => { + const manager = getSpectrumManager(); + + const local = manager.addDataArrayMetadata([createDataArray()]); + + manager.finishAddingDataArrayMetadata(); + + const id = local.array[0].id; + const global = manager.globalSpectrumCollection; + + expect(global.getSpectrum(id)).toBe(local.array[0]); + }); + + test('returns null for null and unknown global IDs', () => { + const manager = getSpectrumManager(); + + manager.addDataArrayMetadata([createDataArray()]); + manager.finishAddingDataArrayMetadata(); + + expect(manager.globalSpectrumCollection.getSpectrum(null)).toBeNull(); + + expect(manager.globalSpectrumCollection.getSpectrum('unknown')).toBeNull(); + }); + + test('returns a frozen global collection', () => { + const manager = getSpectrumManager(); + + manager.finishAddingDataArrayMetadata(); + + expect(Object.isFrozen(manager.globalSpectrumCollection)).toBe(true); + }); + }); +}); + +interface DataArrayOptions { + indexForType?: number; + type?: string; + name?: string; + numComponents?: number; + magnitudeRange?: number[]; + ranges?: number[][]; +} + +function createDataArray({ + indexForType = 0, + type = 'POINT', + name = 'displacement', + numComponents = 3, + magnitudeRange = [0, 10], + ranges = [ + [-1, 1], + [-2, 2], + [-3, 3], + ], +}: DataArrayOptions = {}): VisorVtkDataArray { + return new VisorVtkDataArray({ + indexForType, + type, + name, + numComponents, + magnitudeRange, + ranges, + }); +} + +function addSingleSpectrum(dataArray: VisorVtkDataArray): VisorSpectrumInfo { + const manager = getSpectrumManager(); + + return manager.addDataArrayMetadata([dataArray]).array[0]; +} diff --git a/src/ansys/visor/visor-client/src/jest-tests/VisorVtkDataArray.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/VisorVtkDataArray.test.tsx new file mode 100644 index 00000000..fdebba3f --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/VisorVtkDataArray.test.tsx @@ -0,0 +1,167 @@ +import VisorVtkDataArray from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; + +describe('VisorVtkDataArray', () => { + test('constructs an instance from an object', () => { + const input = createValidInput(); + + const result = new VisorVtkDataArray(input); + + expect(result).toBeInstanceOf(VisorVtkDataArray); + expect(result.indexForType).toBe(2); + expect(result.type).toBe('POINT'); + expect(result.name).toBe('displacement'); + expect(result.numComponents).toBe(3); + expect(result.magnitudeRange).toEqual([0, 10]); + expect(result.ranges).toEqual([ + [-1, 1], + [-2, 2], + [-3, 3], + ]); + }); + + test('constructs an instance from a JSON string', () => { + const input = createValidInput(); + + const result = new VisorVtkDataArray(JSON.stringify(input)); + + expect(result.indexForType).toBe(input.indexForType); + expect(result.type).toBe(input.type); + expect(result.name).toBe(input.name); + expect(result.numComponents).toBe(input.numComponents); + expect(result.magnitudeRange).toEqual(input.magnitudeRange); + expect(result.ranges).toEqual(input.ranges); + }); + + test('uses the original array references when constructed from an object', () => { + const input = createValidInput(); + + const result = new VisorVtkDataArray(input); + + expect(result.magnitudeRange).toBe(input.magnitudeRange); + expect(result.ranges).toBe(input.ranges); + }); + + test('creates new arrays when constructed from JSON', () => { + const input = createValidInput(); + + const result = new VisorVtkDataArray(JSON.stringify(input)); + + expect(result.magnitudeRange).not.toBe(input.magnitudeRange); + expect(result.ranges).not.toBe(input.ranges); + expect(result.magnitudeRange).toEqual(input.magnitudeRange); + expect(result.ranges).toEqual(input.ranges); + }); + + test('ignores additional object properties', () => { + const input = { + ...createValidInput(), + unusedProperty: 'unused', + }; + + const result = new VisorVtkDataArray(input as any); + + expect(result).not.toHaveProperty('unusedProperty'); + }); + + test('throws for malformed JSON', () => { + expect(() => { + new VisorVtkDataArray('{invalid json'); + }).toThrow(SyntaxError); + }); + + test.each([ + [ + 'indexForType', + { + ...createValidInput(), + indexForType: '2', + }, + ], + [ + 'type', + { + ...createValidInput(), + type: 123, + }, + ], + [ + 'name', + { + ...createValidInput(), + name: null, + }, + ], + [ + 'numComponents', + { + ...createValidInput(), + numComponents: '3', + }, + ], + [ + 'magnitudeRange', + { + ...createValidInput(), + magnitudeRange: '0,10', + }, + ], + [ + 'ranges', + { + ...createValidInput(), + ranges: {}, + }, + ], + ])('throws when %s has an invalid type', (_propertyName, input) => { + expect(() => { + new VisorVtkDataArray(input as any); + }).toThrow(); + }); + + test.each(['indexForType', 'type', 'name', 'numComponents', 'magnitudeRange', 'ranges'])( + 'throws when %s is missing', + (propertyName) => { + const input = createValidInput(); + + delete (input as any)[propertyName]; + + expect(() => { + new VisorVtkDataArray(input as any); + }).toThrow(); + } + ); + + test('throws when constructed with null', () => { + expect(() => { + new VisorVtkDataArray(null); + }).toThrow(); + }); + + test('allows empty range arrays because validation only requires arrays', () => { + const input = { + ...createValidInput(), + magnitudeRange: [], + ranges: [], + }; + + const result = new VisorVtkDataArray(input); + + expect(result.magnitudeRange).toEqual([]); + expect(result.ranges).toEqual([]); + }); +}); + +function createValidInput() { + return { + indexForType: 2, + type: 'POINT', + name: 'displacement', + numComponents: 3, + magnitudeRange: [0, 10], + ranges: [ + [-1, 1], + [-2, 2], + [-3, 3], + ], + }; +} diff --git a/src/ansys/visor/visor-client/src/main.tsx b/src/ansys/visor/visor-client/src/main.tsx new file mode 100644 index 00000000..641feb85 --- /dev/null +++ b/src/ansys/visor/visor-client/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './styles/index.css'; +import './styles/inputs.css'; +import './styles/themes/visor-theme-rules.css'; + +createRoot(document.getElementById('VisorContainer')!).render( + + + +); diff --git a/src/ansys/visor/visor-client/src/renderer/IRenderer.ts b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts new file mode 100644 index 00000000..de07a0ac --- /dev/null +++ b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts @@ -0,0 +1,180 @@ +import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; + +/** Stable integer id of an addressable scene-graph node (part or block). */ +export type NodeId = number; + +/** + * Renderer-agnostic camera snapshot. Superset of what UI panels read and what + * getAppStateAsync/setAppStateAsync round-trip. Every field is a plain value + * so a RemoteRenderer can cache and serve it without querying a wasm object. + */ +export type VisorCameraState = Readonly<{ + // Derived display properties (UI panels, camera-changed listeners) + distance: number; + orthographic: boolean; + orthographicScale: number; + unitsPerPixel: number; + viewPortHeight: number; + // Full VTK camera (needed for save / restore) + position: readonly number[]; + focalPoint: readonly number[]; + viewUp: readonly number[]; + clippingRange: readonly number[]; + parallelProjection: boolean; + viewAngle: number; + parallelScale: number; +}>; + +/** Descriptor consumed by setColorVariableAsync. */ +export type ColorVariableDescriptor = Readonly<{ + spectrumId: string; + spectrumType: 'POINT' | 'CELL'; + spectrumName: string; + component: number; // -1 = magnitude + min: number; + max: number; +}>; + +/** Result returned by pickGeometryAsync. Discriminated by `mode`. */ +export type PickGeometryResult = + | { found: false } + | { found: true; mode: 'vertex'; position: readonly number[] } + | { + found: true; + mode: 'edge'; + pointA: readonly number[]; + pointB: readonly number[]; + length: number; + } + | { found: true; mode: 'face'; area: number; points: ReadonlyArray }; + +/** Geometry pick mode. */ +export type GeometryPickMode = 'vertex' | 'edge' | 'face'; + +export interface IRenderer { + // ---- Canvas / lifecycle ------------------------------------------------- + /** DOM element hosting the render output (canvas + overlay). */ + readonly domElement: HTMLDivElement; + /** + * Hand the live scene graph to the renderer. Called by VisorFrontend immediately + * after CreateVisorSceneGraph. Temporary: exists because the bounding-box and + * edges widgets read live per-node bounds and visibility. + * A RemoteRenderer gets those from the server and implements this as a no-op. + * TODO: Remove after user wire contract is updated to allow further decoupling. + */ + attachSceneGraph(sceneGraph: VisorSceneNodeExtended): void; + /** Kick a frame flush. Wasm: wasm.update() + Render(). Remote: no-op / RPC. */ + renderAsync(): Promise; + /** Resize output to its container. Call after container size changes. */ + resizeAsync(): Promise; + /** Drop listeners, wasm observers, overlay canvas, RPC sessions, etc. */ + dispose(): void; + + // ---- Camera (all reads async; state may live off-thread / off-box) ------ + getCameraStateAsync(): Promise; + setCameraPositionAsync(position: readonly number[]): Promise; + setCameraFocalPointAsync(focalPoint: readonly number[]): Promise; + setCameraViewUpAsync(viewUp: readonly number[]): Promise; + setCameraClippingRangeAsync(clippingRange: readonly number[]): Promise; + setCameraParallelProjectionAsync(enabled: boolean): Promise; + setCameraViewAngleAsync(angle: number): Promise; + setCameraParallelScaleAsync(scale: number): Promise; + /** + * Apply an entire camera snapshot in one RPC. WasmRenderer implements it + * by delegating to the seven per-field setters above. See ยง11 for the + * Story 3.2 rationale (server-tracked camera + sync-back). + */ + setCameraStateAsync(state: VisorCameraState): Promise; + /** Frame the scene on the given bounds; used by scene-graph rebuilds. */ + resetCameraAsync(bounds?: readonly number[]): Promise; + + // ---- Subscriptions (return unsubscribe closures) ------------------------ + /** Fires whenever the camera changes. Callback receives a full snapshot. */ + addCameraChangedListener(callback: (state: VisorCameraState) => void): () => void; + /** Fires each frame with the current FPS. */ + addFrameRenderedListener(callback: (fps: number) => void): () => void; + /** + * Fires on left-click. `nodeId` is already resolved (actorโ†’NodeId mapping + * is the renderer's private responsibility โ€” see ยง3). `null` = background. + * `normX`/`normY` are viewport-normalised coords so the caller can drive + * pickGeometryAsync if needed. + */ + addViewerClickedListener( + callback: ( + nodeId: NodeId | null, + ctrlKey: boolean, + shiftKey: boolean, + normX: number, + normY: number + ) => void + ): () => void; + + // ---- Per-part visual state --------------------------------------------- + /** + * r/g/b are normalised (0..1), matching setSelectedAsync's currentDiffuseRgb. + * `selected` is the caller's current selection state for the node: the renderer + * holds no per-part state, and the applied property values depend on it. + */ + setDiffuseColorRgbAsync( + nodeId: NodeId, + r: number, + g: number, + b: number, + selected: boolean + ): Promise; + /** defaultRgb is normalised (0..1). `selected` as above. */ + resetDiffuseColorAsync( + nodeId: NodeId, + defaultRgb: readonly number[], + selected: boolean + ): Promise; + setVisibilityAsync(nodeId: NodeId, visible: boolean): Promise; + setSelectedAsync( + nodeId: NodeId, + selected: boolean, + currentDiffuseRgb: readonly number[] + ): Promise; + setEdgeVisibilityAsync(nodeId: NodeId, edgeVisible: boolean): Promise; + setOpacityAsync(nodeId: NodeId, opacity: number): Promise; + setColorVariableAsync(nodeId: NodeId, descriptor: ColorVariableDescriptor): Promise; + clearColorVariableAsync(nodeId: NodeId): Promise; + setScalarRangeAsync(nodeId: NodeId, min: number, max: number): Promise; + + // ---- View-level widgets (state is renderer-owned; see arch rule (a)) --- + setCrossSectionVisibilityAsync(visible?: boolean): Promise; + isCrossSectionVisible(): boolean; // cached bool, sync + updateCrossSectionBoundsAsync(): Promise; + getCrossSectionOriginAsync(): Promise; + getCrossSectionNormalAsync(): Promise; + setCrossSectionOriginAsync(origin: readonly number[]): Promise; + setCrossSectionNormalAsync(normal: readonly number[]): Promise; + + setBoundingBoxVisibilityAsync(visible?: boolean): Promise; + isBoundingBoxVisible(): boolean; + updateBoundingBoxBoundsAsync(): Promise; + + setOrthographicModeAsync(enable?: boolean): Promise; + isOrthographicEnabled(): boolean; + + setEdgeVisibilityGlobalAsync(visible?: boolean): Promise; + areEdgesVisibleGlobally(): boolean; + + toggleFullScreenAsync(): Promise; + + // ---- Picking (geometry pick โ€” the vertex/edge/face path) --------------- + /** + * Perform a geometry pick at the given viewport-normalised coordinates. + * Actorโ†’NodeId resolution and cell extraction are the renderer's job. + * Returns a discriminated result; caller updates UI. Highlight rendering + * is initiated by setGeometryHighlightAsync. + */ + pickGeometryAsync( + normX: number, + normY: number, + mode: GeometryPickMode + ): Promise; + /** Draw the overlay for the last pick (vertex dot, edge line, face poly). */ + setGeometryHighlightAsync(result: PickGeometryResult): Promise; + /** Remove the overlay. */ + clearGeometryHighlight(): void; +} diff --git a/src/ansys/visor/visor-client/src/renderer/NullRenderer.ts b/src/ansys/visor/visor-client/src/renderer/NullRenderer.ts new file mode 100644 index 00000000..6711cf5e --- /dev/null +++ b/src/ansys/visor/visor-client/src/renderer/NullRenderer.ts @@ -0,0 +1,244 @@ +import { + ColorVariableDescriptor, + GeometryPickMode, + IRenderer, + NodeId, + PickGeometryResult, + VisorCameraState, +} from './IRenderer'; +import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; + +const NULL_CAMERA_STATE: VisorCameraState = { + distance: 0, + orthographic: false, + orthographicScale: 1, + unitsPerPixel: 1, + viewPortHeight: 0, + position: [0, 0, 0], + focalPoint: [0, 0, 0], + viewUp: [0, 0, 0], + clippingRange: [0, 0], + parallelProjection: false, + viewAngle: 30, + parallelScale: 1, +}; + +/** + * No-op implementation of IRenderer. Used to confirm the interface is + * buildable and to serve as a placeholder renderer until a concrete one is + * wired in. Nothing imports this yet. + */ +export class NullRenderer implements IRenderer { + readonly domElement: HTMLDivElement = document.createElement('div'); + + attachSceneGraph(_sceneGraph: VisorSceneNodeExtended): void { + // no-op + } + + async renderAsync(): Promise { + // no-op + } + + async resizeAsync(): Promise { + // no-op + } + + dispose(): void { + // no-op + } + + async getCameraStateAsync(): Promise { + return NULL_CAMERA_STATE; + } + + async setCameraPositionAsync(_position: readonly number[]): Promise { + // no-op + } + + async setCameraFocalPointAsync(_focalPoint: readonly number[]): Promise { + // no-op + } + + async setCameraViewUpAsync(_viewUp: readonly number[]): Promise { + // no-op + } + + async setCameraClippingRangeAsync(_clippingRange: readonly number[]): Promise { + // no-op + } + + async setCameraParallelProjectionAsync(_enabled: boolean): Promise { + // no-op + } + + async setCameraViewAngleAsync(_angle: number): Promise { + // no-op + } + + async setCameraParallelScaleAsync(_scale: number): Promise { + // no-op + } + + async setCameraStateAsync(_state: VisorCameraState): Promise { + // no-op + } + + async resetCameraAsync(_bounds?: readonly number[]): Promise { + // no-op + } + + addCameraChangedListener(_callback: (state: VisorCameraState) => void): () => void { + return () => { + // no-op unsubscribe + }; + } + + addFrameRenderedListener(_callback: (fps: number) => void): () => void { + return () => { + // no-op unsubscribe + }; + } + + addViewerClickedListener( + _callback: ( + nodeId: NodeId | null, + ctrlKey: boolean, + shiftKey: boolean, + normX: number, + normY: number + ) => void + ): () => void { + return () => { + // no-op unsubscribe + }; + } + + async setDiffuseColorRgbAsync( + _nodeId: NodeId, + _r: number, + _g: number, + _b: number, + _selected: boolean + ): Promise { + // no-op + } + + async resetDiffuseColorAsync( + _nodeId: NodeId, + _defaultRgb: readonly number[], + _selected: boolean + ): Promise { + // no-op + } + + async setVisibilityAsync(_nodeId: NodeId, _visible: boolean): Promise { + // no-op + } + + async setSelectedAsync( + _nodeId: NodeId, + _selected: boolean, + _currentDiffuseRgb: readonly number[] + ): Promise { + // no-op + } + + async setEdgeVisibilityAsync(_nodeId: NodeId, _edgeVisible: boolean): Promise { + // no-op + } + + async setOpacityAsync(_nodeId: NodeId, _opacity: number): Promise { + // no-op + } + + async setColorVariableAsync( + _nodeId: NodeId, + _descriptor: ColorVariableDescriptor + ): Promise { + // no-op + } + + async clearColorVariableAsync(_nodeId: NodeId): Promise { + // no-op + } + + async setScalarRangeAsync(_nodeId: NodeId, _min: number, _max: number): Promise { + // no-op + } + + async setCrossSectionVisibilityAsync(_visible?: boolean): Promise { + // no-op + } + + isCrossSectionVisible(): boolean { + return false; + } + + async updateCrossSectionBoundsAsync(): Promise { + // no-op + } + + async getCrossSectionOriginAsync(): Promise { + return [0, 0, 0]; + } + + async getCrossSectionNormalAsync(): Promise { + return [0, 0, 1]; + } + + async setCrossSectionOriginAsync(_origin: readonly number[]): Promise { + // no-op + } + + async setCrossSectionNormalAsync(_normal: readonly number[]): Promise { + // no-op + } + + async setBoundingBoxVisibilityAsync(_visible?: boolean): Promise { + // no-op + } + + isBoundingBoxVisible(): boolean { + return false; + } + + async updateBoundingBoxBoundsAsync(): Promise { + // no-op + } + + async setOrthographicModeAsync(_enable?: boolean): Promise { + // no-op + } + + isOrthographicEnabled(): boolean { + return false; + } + + async setEdgeVisibilityGlobalAsync(_visible?: boolean): Promise { + // no-op + } + + areEdgesVisibleGlobally(): boolean { + return false; + } + + async toggleFullScreenAsync(): Promise { + // no-op + } + + async pickGeometryAsync( + _normX: number, + _normY: number, + _mode: GeometryPickMode + ): Promise { + return { found: false }; + } + + async setGeometryHighlightAsync(_result: PickGeometryResult): Promise { + // no-op + } + + clearGeometryHighlight(): void { + // no-op + } +} diff --git a/src/ansys/visor/visor-client/src/renderer/RendererAnnotation.ts b/src/ansys/visor/visor-client/src/renderer/RendererAnnotation.ts new file mode 100644 index 00000000..2c83e09e --- /dev/null +++ b/src/ansys/visor/visor-client/src/renderer/RendererAnnotation.ts @@ -0,0 +1,133 @@ +import { ensureNumber, ensureString } from '../state/appstate/VisorStateCommon.tsx'; + +/** + * Wasm object-manager ids for one renderable part node. Mirrors + * `WasmNodeHandles` in `ansys.visor.viewer.models.runtime.vtk.renderer_annotation`. + */ +export type WasmNodeHandles = Readonly<{ + actorId: number; + propertyId: number; + mapperId: number; +}>; + +/** + * Wasm object-manager ids for the renderer's singleton widgets. Mirrors + * `WasmWidgetHandles` in `ansys.visor.viewer.models.runtime.vtk.renderer_annotation`. + * Static after renderer initialisation; independent of scene contents. + */ +export type WasmWidgetHandles = Readonly<{ + orientationWidgetId: number; + crossSectionPlaneId: number; + crossSectionPlaneWidgetId: number; + crossSectionPlaneRepresentationId: number; + boundingBoxAlgorithmId: number; + boundingBoxOutlineActorId: number; + boundingBoxAxesActorId: number; +}>; + +/** + * Annotation produced by the wasm (`VisorLocalRenderer`) renderer. Mirrors + * `WasmRendererAnnotation` on the Python side. + * + * `RendererAnnotation` is currently just this one type. It is written as its + * own alias, rather than folded directly into `RendererAnnotation`, so that a + * future second renderer kind can be added as a real discriminated union + * without touching this type's shape. + */ +export type WasmRendererAnnotation = Readonly<{ + rendererKind: 'wasm'; + /** Keyed by String(nodeId). Only part nodes appear. */ + nodes: Readonly>; + widgets: WasmWidgetHandles; +}>; + +/** + * Every renderer's annotation. Today there is exactly one renderer kind that + * produces a non-null annotation on the wire (`'wasm'`); a renderer with no + * client-side handles is represented by absence (`null`). + */ +export type RendererAnnotation = WasmRendererAnnotation; + +function parseWasmNodeHandles(obj: any, path: string): WasmNodeHandles { + ensureNumber(obj.actorId, `${path}.actorId`); + ensureNumber(obj.propertyId, `${path}.propertyId`); + ensureNumber(obj.mapperId, `${path}.mapperId`); + return { + actorId: obj.actorId, + propertyId: obj.propertyId, + mapperId: obj.mapperId, + }; +} + +function parseWasmWidgetHandles(obj: any): WasmWidgetHandles { + ensureNumber(obj.orientationWidgetId, `rendererAnnotation.widgets.orientationWidgetId`); + ensureNumber(obj.crossSectionPlaneId, `rendererAnnotation.widgets.crossSectionPlaneId`); + ensureNumber( + obj.crossSectionPlaneWidgetId, + `rendererAnnotation.widgets.crossSectionPlaneWidgetId` + ); + ensureNumber( + obj.crossSectionPlaneRepresentationId, + `rendererAnnotation.widgets.crossSectionPlaneRepresentationId` + ); + ensureNumber(obj.boundingBoxAlgorithmId, `rendererAnnotation.widgets.boundingBoxAlgorithmId`); + ensureNumber( + obj.boundingBoxOutlineActorId, + `rendererAnnotation.widgets.boundingBoxOutlineActorId` + ); + ensureNumber(obj.boundingBoxAxesActorId, `rendererAnnotation.widgets.boundingBoxAxesActorId`); + return { + orientationWidgetId: obj.orientationWidgetId, + crossSectionPlaneId: obj.crossSectionPlaneId, + crossSectionPlaneWidgetId: obj.crossSectionPlaneWidgetId, + crossSectionPlaneRepresentationId: obj.crossSectionPlaneRepresentationId, + boundingBoxAlgorithmId: obj.boundingBoxAlgorithmId, + boundingBoxOutlineActorId: obj.boundingBoxOutlineActorId, + boundingBoxAxesActorId: obj.boundingBoxAxesActorId, + }; +} + +function parseWasmRendererAnnotation(obj: any): WasmRendererAnnotation { + if (typeof obj.nodes !== 'object' || obj.nodes === null) { + throw new TypeError(`rendererAnnotation.nodes must be an object`); + } + if (typeof obj.widgets !== 'object' || obj.widgets === null) { + throw new TypeError(`rendererAnnotation.widgets must be an object`); + } + const nodes: Record = {}; + for (const nodeId of Object.keys(obj.nodes)) { + nodes[nodeId] = parseWasmNodeHandles( + obj.nodes[nodeId], + `rendererAnnotation.nodes[${nodeId}]` + ); + } + const widgets = parseWasmWidgetHandles(obj.widgets); + return { rendererKind: 'wasm', nodes, widgets }; +} + +/** + * Parse and narrow at the payload boundary. + * + * `null`/`undefined` parse to `null`: a renderer with no client-side handles + * is represented by absence. + * + * Throws on any other `rendererKind`. + */ +export function parseRendererAnnotation(obj: any): RendererAnnotation | null { + if (obj == null) { + return null; + } + ensureString(obj.rendererKind, `rendererAnnotation.rendererKind`); + if (obj.rendererKind === 'wasm') { + return parseWasmRendererAnnotation(obj); + } + throw new TypeError(`Unrecognized rendererAnnotation.rendererKind: '${obj.rendererKind}'`); +} + +/** Narrow to the wasm annotation, or throw if the annotation is absent. */ +export function requireWasmAnnotation(a: RendererAnnotation | null): WasmRendererAnnotation { + if (a == null) { + throw new TypeError(`Expected a wasm renderer annotation, but the annotation is absent`); + } + return a; +} diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts new file mode 100644 index 00000000..91245546 --- /dev/null +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -0,0 +1,428 @@ +import { + ColorVariableDescriptor, + GeometryPickMode, + IRenderer, + NodeId, + PickGeometryResult, + VisorCameraState, +} from './IRenderer'; +import VtkScene from '../wasm/VtkScene'; +import { WasmNodeHandles, WasmRendererAnnotation } from './RendererAnnotation'; +import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import { CrossSectionWidget } from '../widgets/crossSectionWidget.ts'; +import { BoundingBoxWidget } from '../widgets/boundingBoxWidget.ts'; +import { OrthographicWidget } from '../widgets/orthographicWidget.ts'; +import { FullscreenWidget } from '../widgets/fullscreenWidget.ts'; +import { EdgesWidget } from '../widgets/edgesWidget.ts'; + +const SELECTION_TINT_RGB: Readonly = [0 / 255, 62 / 255, 111 / 255]; + +/** + * Local-wasm implementation of {@link IRenderer}. Method bodies here are + * moved verbatim (extraction, not a rewrite) from `VisorFrontend.tsx` and its + * widget classes. `VisorFrontend.tsx` still has its own copies of all of + * this โ€” nothing imports `WasmRenderer` yet. + */ +export class WasmRenderer implements IRenderer { + private constructor(vtkScene: VtkScene, annotation: WasmRendererAnnotation) { + this.#vtkScene = vtkScene; + + const wasmPlaneId = annotation.widgets.crossSectionPlaneId; + const wasmPlaneRepId = annotation.widgets.crossSectionPlaneRepresentationId; + const wasmPlaneWidgetId = annotation.widgets.crossSectionPlaneWidgetId; + + // Both maps are built once, here, from the annotation's per-node + // handles. The actorIdToNodeId map is used to translate the wasm pick result + // to a nodeId. The nodeIdToHandles map is used to look up the handles for + // any nodeId when applying per-node visual state. + for (const [nodeIdKey, handles] of Object.entries(annotation.nodes)) { + const nodeId: NodeId = Number(nodeIdKey); + this.#actorIdToNodeId.set(handles.actorId, nodeId); + this.#nodeIdToHandles.set(nodeId, handles); + } + + // Widgets that only need vtkScene are built here. BoundingBoxWidget and + // EdgesWidget need the live scene graph and are built in attachSceneGraph. + this.#crossSectionWidget = new CrossSectionWidget(vtkScene, { + planeWasmId: wasmPlaneId, + representationWasmId: wasmPlaneRepId, + widgetWasmId: wasmPlaneWidgetId, + }); + this.#orthographicWidget = new OrthographicWidget(vtkScene); + this.#fullscreenWidget = new FullscreenWidget(); + + const planeWidget = vtkScene.getVtkObject(wasmPlaneWidgetId); + planeWidget.observe('InteractionEvent', this.#crossSectionWidget.interactionHandler); + + // Bounding-box ids are stashed for attachSceneGraph, which is the + // point at which the live sceneGraph (needed by BoundingBoxWidget) + // becomes available. + this.#wasmBoundingBoxActorId = annotation.widgets.boundingBoxOutlineActorId; + this.#wasmBoundingBoxAxesActorId = annotation.widgets.boundingBoxAxesActorId; + this.#boundingBoxBoxAlgorithmWasmId = annotation.widgets.boundingBoxAlgorithmId; + + this.domElement = vtkScene.canvasDiv; + } + + static async createAsync( + vtkScene: VtkScene, + annotation: WasmRendererAnnotation + ): Promise { + return new WasmRenderer(vtkScene, annotation); + } + + readonly #vtkScene: VtkScene; + readonly #actorIdToNodeId = new Map(); + readonly #nodeIdToHandles = new Map(); + readonly #crossSectionWidget: CrossSectionWidget; + readonly #orthographicWidget: OrthographicWidget; + readonly #fullscreenWidget: FullscreenWidget; + readonly #wasmBoundingBoxActorId: number; + readonly #wasmBoundingBoxAxesActorId: number; + readonly #boundingBoxBoxAlgorithmWasmId: number; + #boundingBoxWidget: BoundingBoxWidget | null = null; + #edgesWidget: EdgesWidget | null = null; + + #requireSceneGraphAttached(methodName: string): void { + if (this.#boundingBoxWidget == null || this.#edgesWidget == null) { + throw new Error( + `WasmRenderer.${methodName} called before attachSceneGraph(). ` + + 'VisorFrontend must call attachSceneGraph immediately after CreateVisorSceneGraph.' + ); + } + } + + // ---- Canvas / lifecycle ------------------------------------------------- + readonly domElement: HTMLDivElement; + + attachSceneGraph(sceneGraph: VisorSceneNodeExtended): void { + this.#boundingBoxWidget = new BoundingBoxWidget(this.#vtkScene, sceneGraph, { + outlineActorId: this.#wasmBoundingBoxActorId, + axesActorId: this.#wasmBoundingBoxAxesActorId, + boxAlgorithmWasmId: this.#boundingBoxBoxAlgorithmWasmId, + outlineQueryId: this.#wasmBoundingBoxActorId, + }); + this.#edgesWidget = new EdgesWidget(sceneGraph); + } + + async renderAsync(): Promise { + this.#vtkScene.render(); + } + + async resizeAsync(): Promise { + await this.#vtkScene.resizeAsync(); + this.#vtkScene.render(); + } + + dispose(): void { + this.#vtkScene.clearObserversAndEventListeners(); + } + + // ---- Camera --------------------------------------------------------------- + async getCameraStateAsync(): Promise { + const camera = this.#vtkScene.camera; + const parallelScale = await camera.GetParallelScale(); + const viewPortHeight = await this.#vtkScene.getViewPortHeightAsync(); + const parallelProjectionRaw = await camera.GetParallelProjection(); + return { + distance: await camera.GetDistance(), + orthographic: !!parallelProjectionRaw, + orthographicScale: parallelScale, + // unitsPerPixel is only accurate in orthographic mode + unitsPerPixel: parallelScale / (0.5 * viewPortHeight), + viewPortHeight, + position: await camera.GetPosition(), + focalPoint: await camera.GetFocalPoint(), + viewUp: await camera.GetViewUp(), + clippingRange: await camera.GetClippingRange(), + parallelProjection: parallelProjectionRaw === 1, + viewAngle: await camera.GetViewAngle(), + parallelScale, + }; + } + + async setCameraPositionAsync(position: readonly number[]): Promise { + await this.#vtkScene.camera.setPosition(position); + } + + async setCameraFocalPointAsync(focalPoint: readonly number[]): Promise { + await this.#vtkScene.camera.setFocalPoint(focalPoint); + } + + async setCameraViewUpAsync(viewUp: readonly number[]): Promise { + await this.#vtkScene.camera.setViewUp(viewUp); + } + + async setCameraClippingRangeAsync(clippingRange: readonly number[]): Promise { + await this.#vtkScene.camera.setClippingRange(clippingRange); + } + + async setCameraParallelProjectionAsync(enabled: boolean): Promise { + await this.#vtkScene.camera.setParallelProjection(enabled ? 1 : 0); + } + + async setCameraViewAngleAsync(angle: number): Promise { + await this.#vtkScene.camera.setViewAngle(angle); + } + + async setCameraParallelScaleAsync(scale: number): Promise { + await this.#vtkScene.camera.setParallelScale(scale); + } + + async setCameraStateAsync(state: VisorCameraState): Promise { + // Sequential, not Promise.all, to preserve the observable ordering of + // camera events any FPS/camera-changed listener sees (ยง11). + await this.setCameraPositionAsync(state.position); + await this.setCameraFocalPointAsync(state.focalPoint); + await this.setCameraViewUpAsync(state.viewUp); + await this.setCameraClippingRangeAsync(state.clippingRange); + await this.setCameraParallelProjectionAsync(state.parallelProjection); + await this.setCameraViewAngleAsync(state.viewAngle); + await this.setCameraParallelScaleAsync(state.parallelScale); + } + + async resetCameraAsync(bounds?: readonly number[]): Promise { + if (bounds != null) { + await this.#vtkScene.renderer.ResetCamera(bounds); + } else { + await this.#vtkScene.renderer.ResetCamera(); + } + } + + // ---- Subscriptions ---------------------------------------------------- + addCameraChangedListener(callback: (state: VisorCameraState) => void): () => void { + return this.#vtkScene.addCameraChangedListener(async (_) => { + callback(await this.getCameraStateAsync()); + }); + } + + addFrameRenderedListener(callback: (fps: number) => void): () => void { + return this.#vtkScene.addFrameRenderedListener(async (fps) => { + callback(fps); + }); + } + + addViewerClickedListener( + callback: ( + nodeId: NodeId | null, + ctrlKey: boolean, + shiftKey: boolean, + normX: number, + normY: number + ) => void + ): () => void { + return this.#vtkScene.addViewerClickedListener( + async (actorWasmId, ctrlKey, shiftKey, normX, normY) => { + const nodeId = this.#actorIdToNodeId.get(actorWasmId) ?? null; + callback(nodeId, ctrlKey, shiftKey, normX, normY); + } + ); + } + + // ---- Per-part visual state ----------------------------------------------- + async #applyDiffuseColorAsync( + nodeId: NodeId, + colorRgb: readonly number[], + selected: boolean + ): Promise { + const handles = this.#nodeIdToHandles.get(nodeId); + if (handles == null) { + return; + } + const wasmProperty = this.#vtkScene.getVtkObject(handles.propertyId); + if (selected) { + await wasmProperty.SetAmbientColor(SELECTION_TINT_RGB); + await wasmProperty.SetDiffuse(0.5); + await wasmProperty.SetAmbient(0.5); + } else { + await wasmProperty.SetDiffuse(1); + await wasmProperty.SetAmbient(0); + } + await wasmProperty.SetDiffuseColor(...colorRgb); + } + + async setDiffuseColorRgbAsync( + nodeId: NodeId, + r: number, + g: number, + b: number, + selected: boolean + ): Promise { + await this.#applyDiffuseColorAsync(nodeId, [r, g, b], selected); + } + + async resetDiffuseColorAsync( + nodeId: NodeId, + defaultRgb: readonly number[], + selected: boolean + ): Promise { + await this.#applyDiffuseColorAsync(nodeId, defaultRgb, selected); + } + + async setVisibilityAsync(nodeId: NodeId, visible: boolean): Promise { + const handles = this.#nodeIdToHandles.get(nodeId); + if (handles == null) { + return; + } + const wasmActor = this.#vtkScene.getVtkObject(handles.actorId); + await wasmActor.SetVisibility(visible ? 1 : 0); + } + + async setSelectedAsync( + nodeId: NodeId, + selected: boolean, + currentDiffuseRgb: readonly number[] + ): Promise { + await this.#applyDiffuseColorAsync(nodeId, currentDiffuseRgb, selected); + } + + async setEdgeVisibilityAsync(nodeId: NodeId, edgeVisible: boolean): Promise { + const handles = this.#nodeIdToHandles.get(nodeId); + if (handles == null) { + return; + } + const rgb = [0, 0, 0]; + const wasmProperty = this.#vtkScene.getVtkObject(handles.propertyId); + await wasmProperty.SetEdgeColor(rgb); + if (edgeVisible) { + await wasmProperty.EdgeVisibilityOn(); + } else { + await wasmProperty.EdgeVisibilityOff(); + } + } + + async setOpacityAsync(nodeId: NodeId, opacity: number): Promise { + const handles = this.#nodeIdToHandles.get(nodeId); + if (handles == null) { + return; + } + const wasmProperty = this.#vtkScene.getVtkObject(handles.propertyId); + await wasmProperty.SetOpacity(opacity); + } + + async setColorVariableAsync( + nodeId: NodeId, + descriptor: ColorVariableDescriptor + ): Promise { + const handles = this.#nodeIdToHandles.get(nodeId); + if (handles == null) { + return; + } + const wasmMapper = this.#vtkScene.getVtkObject(handles.mapperId); + if (descriptor.spectrumType === 'POINT') { + await wasmMapper.SetScalarModeToUsePointFieldData(); + } else { + await wasmMapper.SetScalarModeToUseCellFieldData(); + } + await wasmMapper.SetScalarRange(descriptor.min, descriptor.max); + await wasmMapper.SetColorModeToMapScalars(); + await wasmMapper.ColorByArrayComponent(descriptor.spectrumName, descriptor.component); + await wasmMapper.SetScalarVisibility(1); + // Force creation of LUT if not already done. (Alternatively, after 9.5.20250802.dev0, you can call mapper.SetLookupTable(null)) + await wasmMapper.CreateDefaultLookupTable(); + const lut = await wasmMapper.GetLookupTable(); + await lut.SetHueRange(0.667, 0.0); + await lut.SetVectorModeToMagnitude(); + } + + async clearColorVariableAsync(nodeId: NodeId): Promise { + const handles = this.#nodeIdToHandles.get(nodeId); + if (handles == null) { + return; + } + const wasmMapper = this.#vtkScene.getVtkObject(handles.mapperId); + await wasmMapper.SetScalarVisibility(0); + } + + async setScalarRangeAsync(nodeId: NodeId, min: number, max: number): Promise { + const handles = this.#nodeIdToHandles.get(nodeId); + if (handles == null) { + return; + } + const wasmMapper = this.#vtkScene.getVtkObject(handles.mapperId); + await wasmMapper.SetScalarRange(min, max); + } + + // ---- View-level widgets ------------------------------------------------- + async setCrossSectionVisibilityAsync(visible?: boolean): Promise { + await this.#crossSectionWidget.setVisibilityAsync(visible); + } + + isCrossSectionVisible(): boolean { + return this.#crossSectionWidget.enabled; + } + + async updateCrossSectionBoundsAsync(): Promise { + await this.#crossSectionWidget.updateBoundsAsync(); + } + + async getCrossSectionOriginAsync(): Promise { + return await this.#crossSectionWidget.getOriginAsync(); + } + + async getCrossSectionNormalAsync(): Promise { + return await this.#crossSectionWidget.getNormalAsync(); + } + + async setCrossSectionOriginAsync(origin: readonly number[]): Promise { + await this.#crossSectionWidget.setOriginAsync(origin as number[]); + } + + async setCrossSectionNormalAsync(normal: readonly number[]): Promise { + await this.#crossSectionWidget.setNormalAsync(normal as number[]); + } + + async setBoundingBoxVisibilityAsync(visible?: boolean): Promise { + this.#requireSceneGraphAttached('setBoundingBoxVisibilityAsync'); + await this.#boundingBoxWidget!.setVisibilityAsync(visible); + } + + isBoundingBoxVisible(): boolean { + this.#requireSceneGraphAttached('isBoundingBoxVisible'); + return this.#boundingBoxWidget!.enabled; + } + + async updateBoundingBoxBoundsAsync(): Promise { + this.#requireSceneGraphAttached('updateBoundingBoxBoundsAsync'); + await this.#boundingBoxWidget!.updateBoundsAsync(); + } + + async setOrthographicModeAsync(enable?: boolean): Promise { + await this.#orthographicWidget.setOrthographicModeAsync(enable); + } + + isOrthographicEnabled(): boolean { + return this.#orthographicWidget.enabled; + } + + async setEdgeVisibilityGlobalAsync(visible?: boolean): Promise { + this.#requireSceneGraphAttached('setEdgeVisibilityGlobalAsync'); + await this.#edgesWidget!.setEdgesVisibleAsync(visible); + } + + areEdgesVisibleGlobally(): boolean { + this.#requireSceneGraphAttached('areEdgesVisibleGlobally'); + return this.#edgesWidget!.enabled; + } + + async toggleFullScreenAsync(): Promise { + await this.#fullscreenWidget.setFullScreenAsync(); + } + + // ---- Picking ------------------------------------------------------------ + async pickGeometryAsync( + normX: number, + normY: number, + mode: GeometryPickMode + ): Promise { + return (await this.#vtkScene.pickGeometryAsync(normX, normY, mode)) as PickGeometryResult; + } + + async setGeometryHighlightAsync(result: PickGeometryResult): Promise { + await this.#vtkScene.setGeometryHighlightAsync(result); + } + + clearGeometryHighlight(): void { + this.#vtkScene.clearGeometryHighlight(); + } +} diff --git a/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx new file mode 100644 index 00000000..bfe8275f --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx @@ -0,0 +1,367 @@ +import VisorColor from '../utils/VisorColor.tsx'; +import { + getSpectrumManager, + VisorSpectrumCollection, + VisorSpectrumManager, +} from './VisorSpectrumManager.tsx'; +import VisorVtkDataArray from './appstate/vtkInfo/VisorVtkDataArray.tsx'; +import { IRenderer } from '../renderer/IRenderer'; + +export type VisorSceneNodeSimple = Readonly<{ + id: number; + dataArrays: VisorVtkDataArray[]; + name: string; + isActorNode: boolean; + isGroupNode: boolean; + nodeType: string; + bounds: number[]; + diffuseColor: number[]; + children: VisorSceneNodeSimple[]; +}>; + +export type VisorSceneNodeExtended = Readonly<{ + id: number; + rootNode: VisorSceneNodeExtended; + name: string; + isActorNode: boolean; + isGroupNode: boolean; + nodeType: string; + bounds: number[]; + children: VisorSceneNodeExtended[]; + diffuseRgb: Readonly; + visible: boolean; + selected: boolean; + edgeVisibility: boolean; + opacity: number; + descendantsOrSelfArray: VisorSceneNodeExtended[]; + descendantsOrSelfDictionary: Record; + descendantsOrSelfDictionaryByPartName: Record; + descendantActorNodesOrSelfArray: VisorSceneNodeExtended[]; + descendantActorNodesOrSelfDictionary: Record; + descendantActorNodesOrSelfDictionaryByPartName: Record; + resetDiffuseColorAsync: () => Promise; + setDiffuseColorHexAsync: (hex: string) => Promise; + setDiffuseColorRgbAsync: (r: number, g: number, b: number) => Promise; + setVisibilityAsync: (visible: boolean) => Promise; + setSelectedAsync: (selected: boolean) => Promise; + setEdgeVisibilityAsync: (edgeVisibility: boolean) => Promise; + setOpacityAsync: (opacity: number) => Promise; + clearColorVariableAsync: () => Promise; + setColorVariableAsync: (id: string, component?: number | null) => Promise; + setScalarRangeAsync: (min: number, max: number) => Promise; + spectrumCollection: VisorSpectrumCollection; + spectrumId: string | null; + spectrumComponent: number; + spectrumMin: number; + spectrumMax: number; + defaultDiffuseColorRgb: Readonly; + defaultDiffuseColorHex: string; + customDiffuseColorRgb: Readonly; + customDiffuseColorHex: string; +}>; + +/** + * @desc If json is provided, this parses the json into an extended Visor scene node object. + * @desc If an existing simple Visor scene node object is provided, this transforms + * the simple scene node object into an extended scene node object. + */ +export const CreateVisorSceneGraph = (() => { + return ( + string_or_object: string | VisorSceneNodeSimple, + spectrumManager?: VisorSpectrumManager, + renderer?: IRenderer + ) => { + let simpleState: VisorSceneNodeSimple; + if (typeof string_or_object === 'string') { + simpleState = JSON.parse(string_or_object); + } else { + simpleState = string_or_object; + } + const localSpectrumManager = spectrumManager == null; + spectrumManager ??= getSpectrumManager(); + const rootNode = extendSimpleNode(simpleState, spectrumManager, null, null, renderer); + localSpectrumManager && spectrumManager.finishAddingDataArrayMetadata(); + return rootNode; + }; + + function extendSimpleNode( + simpleNode: VisorSceneNodeSimple, + spectrumManager: VisorSpectrumManager, + rootNodeSimple?: VisorSceneNodeSimple | null, + rootNodeExtended?: VisorSceneNodeExtended | null, + renderer?: IRenderer + ): VisorSceneNodeExtended { + rootNodeSimple ??= simpleNode; + if (spectrumManager == null) { + throw new Error(`spectrumManager cannot be null`); + } else if (rootNodeSimple.nodeType !== 'root') { + throw new Error(`rootNodeSimple.nodeType must be 'root'`); + } + let _visible: boolean = true; + let _selected: boolean = false; + let _edgeVisibility: boolean = false; + let _opacity: number = 1; + let _spectrumId: string | null = null; + let _spectrumComponent: number = -1; + let _spectrumMin: number = -1; + let _spectrumMax: number = -1; + const defaultDiffuseColor = new VisorColor(); + const customDiffuseColor = new VisorColor(); + if (simpleNode.isActorNode) { + defaultDiffuseColor.setRgb( + simpleNode.diffuseColor[0], + simpleNode.diffuseColor[1], + simpleNode.diffuseColor[2], + true + ); + customDiffuseColor.setHex(defaultDiffuseColor.hex); + } + const nodeId = simpleNode.id; + + const spectrumCollection = spectrumManager.addDataArrayMetadata(simpleNode.dataArrays); + + const node: VisorSceneNodeExtended = { + id: simpleNode.id, + get rootNode() { + return rootNodeExtended!; // this is set later + }, + name: simpleNode.name, + isActorNode: simpleNode.isActorNode, + isGroupNode: simpleNode.isGroupNode, + nodeType: simpleNode.nodeType, + bounds: simpleNode.bounds, + children: [], // this collection is populated later + descendantsOrSelfArray: [], // this collection is populated later + descendantsOrSelfDictionary: {}, // this collection is populated later + descendantsOrSelfDictionaryByPartName: {}, // this collection is populated later + descendantActorNodesOrSelfArray: [], // this collection is populated later + descendantActorNodesOrSelfDictionary: {}, // this collection is populated later + descendantActorNodesOrSelfDictionaryByPartName: {}, // this collection is populated later + get diffuseRgb() { + return customDiffuseColor.rgbNormalized; + }, + get visible() { + return _visible; + }, + get selected() { + return _selected; + }, + get edgeVisibility() { + return _edgeVisibility; + }, + get opacity() { + return _opacity; + }, + spectrumCollection, + async resetDiffuseColorAsync() { + customDiffuseColor.setHex(defaultDiffuseColor.hex); + await renderer!.resetDiffuseColorAsync( + nodeId, + customDiffuseColor.rgbNormalized, + _selected + ); + }, + async setDiffuseColorHexAsync(hex) { + customDiffuseColor.setHex(hex); + const rgbNormalized = customDiffuseColor.rgbNormalized; + await renderer!.setDiffuseColorRgbAsync( + nodeId, + rgbNormalized[0], + rgbNormalized[1], + rgbNormalized[2], + _selected + ); + }, + async setDiffuseColorRgbAsync(r, g, b) { + customDiffuseColor.setRgb(r, g, b); + const rgbNormalized = customDiffuseColor.rgbNormalized; + await renderer!.setDiffuseColorRgbAsync( + nodeId, + rgbNormalized[0], + rgbNormalized[1], + rgbNormalized[2], + _selected + ); + }, + async clearColorVariableAsync() { + _spectrumId = null; + _spectrumComponent = -1; + _spectrumMin = -1; + _spectrumMax = -1; + await renderer!.clearColorVariableAsync(nodeId); + }, + async setColorVariableAsync(id, component) { + if (_spectrumId === id && _spectrumComponent === component) { + return; + } else if (component == null) { + return; + } + const spectrum = spectrumCollection.getSpectrum(id); + if (spectrum == null) { + return; + } + const rangeInfo = spectrum.getRangeInfo(component); + if (rangeInfo == null) { + return; + } + const min = rangeInfo.customRange[0]; + const max = rangeInfo.customRange[1]; + _spectrumId = id; + _spectrumComponent = component; + _spectrumMin = min; + _spectrumMax = max; + await renderer!.setColorVariableAsync(nodeId, { + spectrumId: id, + spectrumType: spectrum.type, + spectrumName: spectrum.name, + component, + min, + max, + }); + }, + async setScalarRangeAsync(min, max) { + await renderer!.setScalarRangeAsync(nodeId, min, max); + }, + async setVisibilityAsync(visible) { + if (_visible === visible) { + return; + } + _visible = visible; + if (node.isGroupNode) { + const promises = []; + for (const n of node.descendantActorNodesOrSelfArray) { + promises.push(n.setVisibilityAsync(visible)); + } + await Promise.all(promises); + return; + } + await renderer!.setVisibilityAsync(nodeId, visible); + }, + async setSelectedAsync(selected) { + if (_selected === selected) { + return; + } + _selected = selected; + if (node.isGroupNode) { + const promises = []; + for (const n of node.descendantActorNodesOrSelfArray) { + promises.push(n.setSelectedAsync(selected)); + } + await Promise.all(promises); + return; + } + await renderer!.setSelectedAsync( + nodeId, + _selected, + customDiffuseColor.rgbNormalized + ); + }, + async setEdgeVisibilityAsync(edgeVisibility: boolean) { + if (_edgeVisibility === edgeVisibility) { + return; + } + _edgeVisibility = edgeVisibility; + if (node.isGroupNode) { + const promises = []; + for (const n of node.descendantActorNodesOrSelfArray) { + promises.push(n.setEdgeVisibilityAsync(edgeVisibility)); + } + await Promise.all(promises); + return; + } + await renderer!.setEdgeVisibilityAsync(nodeId, edgeVisibility); + }, + async setOpacityAsync(opacity: number) { + if (_opacity === opacity) { + return; + } else if (opacity < 0 || opacity > 1) { + throw new Error(`opacity must be a number between 0 and 1 (${opacity})`); + } + _opacity = opacity; + if (node.isGroupNode) { + const promises = []; + for (const n of node.descendantActorNodesOrSelfArray) { + promises.push(n.setOpacityAsync(opacity)); + } + await Promise.all(promises); + return; + } + await renderer!.setOpacityAsync(nodeId, opacity); + }, + get spectrumId() { + return _spectrumId; + }, + get spectrumComponent(): number { + return _spectrumComponent; + }, + get spectrumMin(): number { + return _spectrumMin; + }, + get spectrumMax(): number { + return _spectrumMax; + }, + get customDiffuseColorRgb() { + return customDiffuseColor.rgb; + }, + get customDiffuseColorHex() { + return customDiffuseColor.hex; + }, + get defaultDiffuseColorRgb() { + return defaultDiffuseColor.rgb; + }, + get defaultDiffuseColorHex() { + return defaultDiffuseColor.hex; + }, + }; + rootNodeExtended ??= node; + if (rootNodeSimple.nodeType !== 'root') { + throw new Error(`rootNodeSimple.nodeType must be 'root'`); + } + node.descendantsOrSelfArray.push(node); + node.descendantsOrSelfDictionary[node.id] = node; + node.descendantsOrSelfDictionaryByPartName[node.name] = node; + if (node.isActorNode) { + node.descendantActorNodesOrSelfArray.push(node); + node.descendantActorNodesOrSelfDictionary[node.id] = node; + node.descendantActorNodesOrSelfDictionaryByPartName[node.name] = node; + } + simpleNode.children.forEach((simpleChild) => { + const extendedChild = extendSimpleNode( + simpleChild, + spectrumManager, + rootNodeSimple, + rootNodeExtended, + renderer + ); + node.children.push(extendedChild); + node.descendantsOrSelfArray.push(...extendedChild.descendantsOrSelfArray); + Object.assign( + node.descendantsOrSelfDictionary, + extendedChild.descendantsOrSelfDictionary + ); + Object.assign( + node.descendantsOrSelfDictionaryByPartName, + extendedChild.descendantsOrSelfDictionaryByPartName + ); + node.descendantActorNodesOrSelfArray.push( + ...extendedChild.descendantActorNodesOrSelfArray + ); + Object.assign( + node.descendantActorNodesOrSelfDictionary, + extendedChild.descendantActorNodesOrSelfDictionary + ); + Object.assign( + node.descendantActorNodesOrSelfDictionaryByPartName, + extendedChild.descendantActorNodesOrSelfDictionaryByPartName + ); + }); + Object.freeze(node.children); + Object.freeze(node.descendantsOrSelfArray); + Object.freeze(node.descendantsOrSelfDictionary); + Object.freeze(node.descendantsOrSelfDictionaryByPartName); + Object.freeze(node.descendantActorNodesOrSelfArray); + Object.freeze(node.descendantActorNodesOrSelfDictionary); + Object.freeze(node.descendantActorNodesOrSelfDictionaryByPartName); + return Object.freeze(node); + } +})(); diff --git a/src/ansys/visor/visor-client/src/state/VisorSpectrumManager.tsx b/src/ansys/visor/visor-client/src/state/VisorSpectrumManager.tsx new file mode 100644 index 00000000..e5b96c8b --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/VisorSpectrumManager.tsx @@ -0,0 +1,405 @@ +import VisorVtkDataArray, { FieldAssociation } from './appstate/vtkInfo/VisorVtkDataArray.tsx'; + +/** + * Describes a selectable component of a spectrum. + * + * @remarks + * The magnitude component uses an ID of `-1`. Individual array components + * begin at `0`. + */ +export type VisorSpectrumComponentMetadata = Readonly<{ + /** Numeric identifier used to select the component. */ + id: number; + + /** Human-readable component label, such as `"Magnitude"`, `"X"`, or `"Y"`. */ + name: string; +}>; + +/** + * Describes a spectrum derived from one or more compatible VTK data arrays. + * + * @remarks + * Spectrum objects are immutable, although their custom ranges can be changed + * through {@link VisorSpectrumInfo.setCustomRange}. + */ +export type VisorSpectrumInfo = Readonly<{ + /** + * Unique spectrum identifier. + * + * @remarks + * The identifier has the format + * `"::::"`. + */ + id: string; + + /** Data-array association type, can be either 'POINT' or 'CELL'. */ + type: FieldAssociation; + + /** Name of the underlying data array. */ + name: string; + + /** Human-readable shape name, such as `"Scalar"` or `"Vector3"`. */ + shape: string; + + /** Display name containing the association type, array name, and shape. */ + fullName: string; + + /** Number of components in the underlying data array. */ + numComponents: number; + + /** Components that can be selected when displaying the spectrum. */ + componentOptions: VisorSpectrumComponentMetadata[]; + + /** + * Gets the default and custom ranges for a component. + * + * @param component - Component index. Use `-1` for magnitude, `0` for the + * first array component, or `null`/`undefined` when no component is selected. + * @returns Cloned default and custom ranges, or `null` when the component is + * missing or outside the supported range. + */ + getRangeInfo: (component: number | null | undefined) => null | { + /** Aggregate range calculated from all matching data arrays. */ + defaultRange: number[]; + + /** User-configurable range for the selected component. */ + customRange: number[]; + }; + + /** + * Updates the custom range for a component. + * + * @param component - Component index. Use `-1` for magnitude. + * @param min - New minimum value. + * @param max - New maximum value. + * + * @remarks + * No change is made when the component index is outside the supported range. + */ + setCustomRange: (component: number, min: number, max: number) => void; +}>; + +/** + * A collection of spectra associated with a group of data arrays. + */ +export type VisorSpectrumCollection = Readonly<{ + /** Spectra in collection order. */ + array: VisorSpectrumInfo[]; + + /** + * Finds a spectrum by its unique identifier. + * + * @param id - Spectrum identifier, or `null` when no spectrum is selected. + * @returns The matching spectrum, or `null` when no match exists. + */ + getSpectrum: (id: string | null) => VisorSpectrumInfo | null; +}>; + +/** + * Coordinates spectrum metadata across multiple groups of VTK data arrays. + * + * @remarks + * Call {@link VisorSpectrumManager.addDataArrayMetadata} for every relevant + * group of arrays, then call + * {@link VisorSpectrumManager.finishAddingDataArrayMetadata} once. The global + * collection is unavailable until finalization is complete. + */ +export type VisorSpectrumManager = Readonly<{ + /** + * Adds metadata for a group of VTK data arrays. + * + * @param dataArrays - Data arrays from which spectrum metadata is derived. + * @returns A collection containing one spectrum for each supplied data array. + * + * @remarks + * Arrays with the same type, name, and component count share a global + * spectrum. Their default ranges are expanded to include all observed values. + */ + addDataArrayMetadata: (dataArrays: VisorVtkDataArray[]) => VisorSpectrumCollection; + + /** + * Finalizes the global spectrum collection. + * + * @throws Error if this method has already been called. + */ + finishAddingDataArrayMetadata: () => void; + + /** + * Finalized collection of all globally registered spectra. + * + * @throws Error if + * {@link VisorSpectrumManager.finishAddingDataArrayMetadata} has not yet + * been called. + */ + globalSpectrumCollection: VisorSpectrumCollection; +}>; + +/** + * Creates a spectrum manager for aggregating metadata from VTK data arrays. + * + * @returns A new spectrum manager with no registered spectra. + */ +export function getSpectrumManager(): VisorSpectrumManager { + /** Tracks spectrum IDs and their assigned lookup positions. */ + const spectrumIdLookup: Map = new Map(); + + /** Stores each globally unique spectrum by its ID. */ + const globalSpectrumMap: Map = new Map(); + + /** Stores aggregate default ranges for each spectrum. */ + const globalDefaultRanges: Map = new Map(); + + /** Stores user-configurable ranges for each spectrum. */ + const globalCustomRanges: Map = new Map(); + + /** Maps supported component counts to shape and component-label metadata. */ + const labelInfoMap: Map< + number, + { + /** Human-readable shape name. */ + shape: string; + + /** + * Component labels in display order. + * + * @remarks + * The first label represents magnitude. Remaining labels correspond to + * the underlying array components. + */ + componentLabels: string[]; + } + > = new Map(); + + labelInfoMap.set(1, { + shape: 'Scalar', + componentLabels: ['Magnitude'], + }); + + labelInfoMap.set(2, { + shape: 'Vector2', + componentLabels: ['Magnitude', 'X', 'Y'], + }); + + labelInfoMap.set(3, { + shape: 'Vector3', + componentLabels: ['Magnitude', 'X', 'Y', 'Z'], + }); + + labelInfoMap.set(4, { + shape: 'Vector4', + componentLabels: ['Magnitude', 'X', 'Y', 'Z', 'W'], + }); + + labelInfoMap.set(9, { + shape: 'Vector4', + componentLabels: ['Magnitude', 'XX', 'XY', 'XZ', 'YX', 'YY', 'YZ', 'ZX', 'ZY', 'ZZ'], + }); + + /** Finalized global collection, or `null` until registration is complete. */ + let globalSpectrumCollection: VisorSpectrumCollection | null = null; + + return Object.freeze({ + addDataArrayMetadata, + + /** + * Finalizes the global collection after all data-array metadata has been added. + * + * @throws Error if the global collection has already been finalized. + */ + finishAddingDataArrayMetadata() { + if (globalSpectrumCollection != null) { + throw new Error(`finishAddingDataArrayMetadata() has already been called`); + } + + const array = []; + for (const item of globalSpectrumMap.values()) { + array.push(item); + } + + globalSpectrumCollection = Object.freeze({ + array, + + /** + * Finds a globally registered spectrum. + * + * @param id - Spectrum identifier, or `null`. + * @returns The matching spectrum, or `null` when none exists. + */ + getSpectrum(id: string | null) { + return id != null ? (globalSpectrumMap.get(id) ?? null) : null; + }, + }); + }, + + /** + * Gets the finalized global spectrum collection. + * + * @throws Error if metadata registration has not yet been finalized. + */ + get globalSpectrumCollection() { + if (globalSpectrumCollection == null) { + throw new Error(`finishAddingDataArrayMetadata() has not been called yet`); + } + + return globalSpectrumCollection; + }, + }); + + /** + * Creates or updates spectrum information for a data array. + * + * @param dataArray - Source data array metadata. + * @returns The newly created spectrum, or the existing compatible spectrum. + * + * @remarks + * When a compatible spectrum already exists, its default ranges are expanded + * to include the new array's ranges. Its custom ranges are then reset to the + * updated defaults. + * + * @throws Error if the data array has an unsupported component count. + */ + function tryAddSpectrumInfo(dataArray: VisorVtkDataArray): VisorSpectrumInfo { + const { type, name, numComponents, magnitudeRange, ranges } = dataArray; + + // Use a human-readable ID like 'point::displacement::3' + const id = `${type}::${name}::${numComponents}`; + + if (globalSpectrumMap.has(id)) { + // Update the existing ranges for this spectrum + // with each subsequent new set of ranges. + const defaultRanges = globalDefaultRanges.get(id)!; + const customRanges = globalCustomRanges.get(id)!; + + magnitudeRange[0] < defaultRanges[0][0] && (defaultRanges[0][0] = magnitudeRange[0]); + magnitudeRange[1] > defaultRanges[0][1] && (defaultRanges[0][1] = magnitudeRange[1]); + + ranges.forEach((range, i) => { + range[0] < defaultRanges[i + 1][0] && (defaultRanges[i + 1][0] = range[0]); + range[1] > defaultRanges[i + 1][1] && (defaultRanges[i + 1][1] = range[1]); + }); + + customRanges.forEach((range, i) => { + range[0] = defaultRanges[i][0]; + range[1] = defaultRanges[i][1]; + }); + + return globalSpectrumMap.get(id)!; + } + + const labelInfo = labelInfoMap.get(numComponents); + if (labelInfo == null) { + const msg = `No label info was found for this data array. Do we support label info`; + throw new Error(`${msg} for data arrays with ${numComponents} component(s)?`); + } + + const componentOptions: VisorSpectrumComponentMetadata[] = []; + for (let i = 0; i < labelInfo.componentLabels.length; i++) { + componentOptions.push({ + id: i - 1, + name: labelInfo.componentLabels[i], + }); + } + + const [defaultRanges, customRanges] = (() => { + const arr: number[][] = []; + const arrClone: number[][] = []; + + arr.push([magnitudeRange[0], magnitudeRange[1]]); + arrClone.push([magnitudeRange[0], magnitudeRange[1]]); + + for (let i = 0; i < numComponents; i++) { + arr.push([ranges[i][0], ranges[i][1]]); + arrClone.push([ranges[i][0], ranges[i][1]]); + } + + return [arr, arrClone]; + })(); + + globalDefaultRanges.set(id, defaultRanges); + globalCustomRanges.set(id, customRanges); + + const info: VisorSpectrumInfo = Object.freeze({ + id, + type, + name, + shape: labelInfo.shape, + fullName: `${type} - ${name} (${labelInfo.shape})`, + numComponents, + componentOptions, + + /** + * Gets cloned range information for a component. + * + * @param component - Component index, with `-1` representing magnitude. + * @returns Range information, or `null` for an invalid component. + */ + getRangeInfo: (component) => { + if (component == null) { + return null; + } + + const i = component + 1; // Add 1 because "magnitude" occupies index 0. + + return i >= 0 && i < customRanges.length + ? { + // Clone the arrays so they cannot be directly modified by the user. + defaultRange: [...defaultRanges[i]], + customRange: [...customRanges[i]], + } + : null; + }, + + /** + * Changes the custom range for a component. + * + * @param component - Component index, with `-1` representing magnitude. + * @param min - New minimum range value. + * @param max - New maximum range value. + */ + setCustomRange: (component, min, max) => { + const i = component + 1; // Add 1 because "magnitude" occupies index 0. + + if (i >= 0 && i < customRanges.length) { + customRanges[i][0] = min; + customRanges[i][1] = max; + } + }, + }); + + globalSpectrumMap.set(id, info); + return info; + } + + /** + * Registers a group of data arrays and creates its local spectrum collection. + * + * @param dataArrays - Data arrays to register. + * @returns An immutable collection containing spectra for the supplied arrays. + */ + function addDataArrayMetadata(dataArrays: VisorVtkDataArray[]): VisorSpectrumCollection { + const array: VisorSpectrumInfo[] = []; + const map: Map = new Map(); + + for (let i = 0; i < dataArrays.length; i++) { + const info = tryAddSpectrumInfo(dataArrays[i]); + array.push(info); + map.set(info.id, info); + } + + Object.freeze(array); + + return Object.freeze({ + array, + + /** + * Finds a spectrum within this local collection. + * + * @param id - Spectrum identifier, or `null`. + * @returns The matching spectrum, or `null` when none exists. + */ + getSpectrum(id: string | null) { + return id != null ? (map.get(id) ?? null) : null; + }, + }); + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorAppState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorAppState.tsx new file mode 100644 index 00000000..a81c3de5 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorAppState.tsx @@ -0,0 +1,47 @@ +import VisorUiState from './VisorUiState.tsx'; +import VisorSceneState from './VisorSceneState.tsx'; +import { JsonDict, parseState, StateInput } from './VisorStateCommon.tsx'; + +export default class VisorAppState { + private _ui: VisorUiState = new VisorUiState(); + private _scene: VisorSceneState = new VisorSceneState(); + + constructor(state: StateInput = null) { + this.copy(state); + } + + get ui(): VisorUiState { + return this._ui; + } + + get scene(): VisorSceneState { + return this._scene; + } + + copy(state: StateInput = null, replace = false): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorAppState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + this._ui.copy(data.ui, replace); + this._scene.copy(data.scene, replace); + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + return { + ui: this.ui.toDict(), + scene: this.scene.toDict(), + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorCameraState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorCameraState.tsx new file mode 100644 index 00000000..f7da239e --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorCameraState.tsx @@ -0,0 +1,181 @@ +import { + ensureBoolean, + ensureNumber, + ensureNumberArray, + JsonDict, + parseState, + StateInput, +} from './VisorStateCommon.tsx'; + +export default class VisorCameraState { + private _position: number[] | undefined = undefined; + private _focalPoint: number[] | undefined = undefined; + private _viewUp: number[] | undefined = undefined; + private _clippingRange: number[] | undefined = undefined; + private _parallelProjection: boolean | undefined = undefined; + private _viewAngle: number | undefined = undefined; + private _parallelScale: number | undefined = undefined; + + constructor(state: StateInput = null) { + this.copy(state); + } + + get position(): number[] | undefined { + return this._position; + } + + setPosition(val: number[] | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._position = val; + } + } else { + ensureNumberArray(val, 'val', 3); + this._position = val; + } + } + + get focalPoint(): number[] | undefined { + return this._focalPoint; + } + + setFocalPoint(val: number[] | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._focalPoint = val; + } + } else { + ensureNumberArray(val, 'val', 3); + this._focalPoint = val; + } + } + + get viewUp(): number[] | undefined { + return this._viewUp; + } + + setViewUp(val: number[] | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._viewUp = val; + } + } else { + ensureNumberArray(val, 'val', 3); + this._viewUp = val; + } + } + + get clippingRange(): number[] | undefined { + return this._clippingRange; + } + + setClippingRange(val: number[] | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._clippingRange = val; + } + } else { + ensureNumberArray(val, 'val', 2); + this._clippingRange = val; + } + } + + get parallelProjection(): boolean | undefined { + return this._parallelProjection; + } + + setParallelProjection(val: boolean | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._parallelProjection = val; + } + } else { + ensureBoolean(val, 'val'); + this._parallelProjection = val; + } + } + + get viewAngle(): number | undefined { + return this._viewAngle; + } + + setViewAngle(val: number | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._viewAngle = val; + } + } else { + ensureNumber(val, 'val'); + this._viewAngle = val; + } + } + + get parallelScale(): number | undefined { + return this._parallelScale; + } + + setParallelScale(val: number | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._parallelScale = val; + } + } else { + ensureNumber(val, 'val'); + this._parallelScale = val; + } + } + + copy(state: StateInput = null, replace = false): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorCameraState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + this.setPosition(data.position === undefined ? this._position : data.position, replace); + this.setFocalPoint( + data.focalPoint === undefined ? this._focalPoint : data.focalPoint, + replace + ); + this.setViewUp(data.viewUp === undefined ? this._viewUp : data.viewUp, replace); + this.setClippingRange( + data.clippingRange === undefined ? this._clippingRange : data.clippingRange, + replace + ); + this.setParallelProjection( + data.parallelProjection === undefined + ? this._parallelProjection + : data.parallelProjection, + replace + ); + this.setViewAngle( + data.viewAngle === undefined ? this._viewAngle : data.viewAngle, + replace + ); + this.setParallelScale( + data.parallelScale === undefined ? this._parallelScale : data.parallelScale, + replace + ); + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + return { + position: this.position, + focalPoint: this.focalPoint, + viewUp: this.viewUp, + clippingRange: this.clippingRange, + parallelProjection: this.parallelProjection, + viewAngle: this.viewAngle, + parallelScale: this.parallelScale, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorCrossSectionState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorCrossSectionState.tsx new file mode 100644 index 00000000..c7b02dc2 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorCrossSectionState.tsx @@ -0,0 +1,67 @@ +import { ensureNumberArray, JsonDict, parseState, StateInput } from './VisorStateCommon.tsx'; + +export default class VisorCrossSectionState { + private _origin: number[] | undefined = undefined; + private _normal: number[] | undefined = undefined; + + constructor(state: StateInput = null) { + this.copy(state); + } + + get origin(): number[] | undefined { + return this._origin; + } + + setOrigin(val: number[] | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._origin = val; + } + } else { + ensureNumberArray(val, 'val', 3); + this._origin = val; + } + } + + get normal(): number[] | undefined { + return this._normal; + } + + setNormal(val: number[] | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._normal = val; + } + } else { + ensureNumberArray(val, 'val', 3); + this._normal = val; + } + } + + copy(state: StateInput = null, replace = false): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorCrossSectionState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + this.setOrigin(data.origin === undefined ? this._origin : data.origin, replace); + this.setNormal(data.normal === undefined ? this._normal : data.normal, replace); + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + return { + origin: this.origin, + normal: this.normal, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorDatasetState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorDatasetState.tsx new file mode 100644 index 00000000..711d30cb --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorDatasetState.tsx @@ -0,0 +1,160 @@ +import VisorPartState from './VisorPartState.tsx'; +import { ensureString, JsonDict, parseState, StateInput } from './VisorStateCommon.tsx'; + +export default class VisorDatasetState { + private _id: string = ''; + private _keyedByName: boolean = false; + private _name: string = ''; + private _partStates: Record = {}; + + constructor( + state: StateInput = null, + key: string | number | null | undefined = null + ) { + this.copy(state, false, key); + } + + get id(): string { + return this._id; + } + + setId(val: string | number | null | undefined): void { + if (val != null) { + this._id = val.toString(); + } + } + + get keyedByName(): boolean { + return this._keyedByName; + } + + setKeyedByName(val: boolean | null | undefined): void { + this._keyedByName = val ?? false; + } + + get name(): string { + return this._name; + } + + setName(val: string): void { + if (val != null) { + this._name = val; + } + } + + getPartState(idStr: string): VisorPartState | null { + ensureString(idStr, 'idStr'); + return this._partStates[idStr] ?? null; + } + + getPartStates(): VisorPartState[] { + return Object.values(this._partStates); + } + + copyPart( + partState: StateInput, + replace = false, + key: string | number | null | undefined = null + ): VisorPartState | null { + if (partState == null) { + return null; + } + + const newState = new VisorPartState(partState, key); + + let existing; + if (newState.keyedByName) { + existing = this._partStates[newState.name]; + } else { + existing = this._partStates[newState.id]; + } + + if (existing) { + return existing.copy(newState, replace); + } + + if (newState.keyedByName) { + return (this._partStates[newState.name] = newState); + } + return (this._partStates[newState.id] = newState); + } + + removePart(idStr: string): boolean { + return delete this._partStates[idStr]; + } + + copy( + state: StateInput = null, + replace = false, + key: string | number | null | undefined = null + ): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorDatasetState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + const keyedByName = data.keyedByName ?? false; + const thisId = (data.id ?? this._id)?.toString(); + const thisName = (data.name ?? this._name)?.toString(); + + if ((key = key?.toString()) != null) { + if (keyedByName) { + if (key !== thisName) { + throw new Error( + `Part state with name '${thisName}' does not equal key '${key}'` + ); + } + } else { + if (key !== thisId) { + throw new Error( + `Part state with id '${thisId}' does not equal key '${key}'` + ); + } + } + } + + this.setId(thisId); + this.setKeyedByName(keyedByName); + this.setName(thisName); + + const partStates = data.partStates; + if (partStates) { + for (const [k, v] of Object.entries(partStates)) { + this.copyPart(v as JsonDict, replace, k); + } + + if (replace) { + for (const k of Object.keys(this._partStates)) { + if (!partStates[k]) { + delete this._partStates[k]; + } + } + } + } + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + const partStates: JsonDict = {}; + for (const [k, v] of Object.entries(this._partStates)) { + if ((v.keyedByName && k === v.name) || k === v.id) { + partStates[k] = v.toDict(); + } + } + return { + id: this.id, + keyedByName: this.keyedByName, + name: this.name, + partStates, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorPartState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorPartState.tsx new file mode 100644 index 00000000..bb2da2e2 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorPartState.tsx @@ -0,0 +1,231 @@ +import { + ensureBoolean, + ensureNumber, + ensureString, + ensureStringOrNull, + ensureNumberArray, + JsonDict, + parseState, + StateInput, +} from './VisorStateCommon.tsx'; + +export default class VisorPartState { + private _id: string = ''; + private _keyedByName: boolean = false; + private _name: string = ''; + private _opacity: number | undefined = undefined; + private _visible: boolean | undefined = undefined; + private _diffuseRgb: Readonly | number[] | undefined = undefined; + private _selected: boolean | undefined = undefined; + private _spectrumId: string | null | undefined = undefined; + private _spectrumComponent: number | undefined = undefined; + + constructor( + state: StateInput = null, + key: string | number | null | undefined = null + ) { + this.copy(state, false, key); + } + + get id(): string { + return this._id; + } + + setId(val: string | number | null | undefined): void { + if (val != null) { + this._id = val.toString(); + } + } + + get keyedByName(): boolean { + return this._keyedByName; + } + + setKeyedByName(val: boolean | null | undefined): void { + this._keyedByName = val ?? false; + } + + get name(): string { + return this._name; + } + + setName(val: string): void { + if (val != null) { + this._name = val; + } + } + + get opacity(): number | undefined { + return this._opacity; + } + + setOpacity(val: number | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._opacity = val; + } + } else { + ensureNumber(val, 'val'); + this._opacity = val; + } + } + + get visible(): boolean | undefined { + return this._visible; + } + + setVisible(val: boolean | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._visible = val; + } + } else { + ensureBoolean(val, 'val'); + this._visible = val; + } + } + + get diffuseRgb(): Readonly | number[] | undefined { + return this._diffuseRgb; + } + + setDiffuseRgb(val: Readonly | number[] | undefined, replace = false): void { + if (val == undefined) { + if (replace) { + this._diffuseRgb = val; + } + } else { + ensureNumberArray(val, 'val'); + const [r, g, b] = val; + if (r < 0 || r > 1) { + throw new Error('diffuseRgb[0] (r) must be between 0 and 1'); + } else if (g < 0 || g > 1) { + throw new Error('diffuseRgb[1] (g) must be between 0 and 1'); + } else if (b < 0 || b > 1) { + throw new Error('diffuseRgb[2] (b) must be between 0 and 1'); + } + this._diffuseRgb = val; + } + } + + get selected(): boolean | undefined { + return this._selected; + } + + setSelected(val: boolean | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._selected = val; + } + } else { + ensureBoolean(val, 'val'); + this._selected = val; + } + } + + get spectrumId(): string | null | undefined { + return this._spectrumId; + } + + setSpectrumId(val: string | null | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._spectrumId = val; + } + } else { + ensureStringOrNull(val, 'val'); + this._spectrumId = val; + } + } + + get spectrumComponent(): number | undefined { + return this._spectrumComponent; + } + + setSpectrumComponent(val: number | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._spectrumComponent = val; + } + } else { + ensureNumber(val, 'val'); + this._spectrumComponent = val; + } + } + + copy( + state: StateInput = null, + replace = false, + key: string | number | null | undefined = null + ): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorPartState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + const keyedByName = data.keyedByName ?? false; + const thisId = (data.id ?? this._id)?.toString(); + const thisName = (data.name ?? this._name)?.toString(); + + if ((key = key?.toString()) != null) { + if (keyedByName) { + if (key !== thisName) { + throw new Error( + `Part state with name '${thisName}' does not equal key '${key}'` + ); + } + } else { + if (key !== thisId) { + throw new Error( + `Part state with id '${thisId}' does not equal key '${key}'` + ); + } + } + } + + this.setId(thisId); + this.setKeyedByName(keyedByName); + this.setName(thisName); + this.setOpacity(data.opacity === undefined ? this._opacity : data.opacity, replace); + this.setVisible(data.visible === undefined ? this._visible : data.visible, replace); + this.setDiffuseRgb( + data.diffuseRgb === undefined ? this._diffuseRgb : data.diffuseRgb, + replace + ); + this.setSelected(data.selected === undefined ? this._selected : data.selected, replace); + this.setSpectrumId( + data.spectrumId === undefined ? this._spectrumId : data.spectrumId, + replace + ); + this.setSpectrumComponent( + data.spectrumComponent === undefined + ? this._spectrumComponent + : data.spectrumComponent, + replace + ); + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + return { + id: this.id, + keyedByName: this.keyedByName, + name: this.name, + opacity: this.opacity, + visible: this.visible, + diffuseRgb: this.diffuseRgb, + selected: this.selected, + spectrumId: this.spectrumId, + spectrumComponent: this.spectrumComponent, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorSceneDetails.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneDetails.tsx new file mode 100644 index 00000000..4a356cb0 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneDetails.tsx @@ -0,0 +1,50 @@ +import { ensureNumber, StateInput } from './VisorStateCommon.tsx'; +import VisorVtkInfo from './vtkInfo/VisorVtkInfo.tsx'; +import VisorAppState from './VisorAppState.tsx'; + +/** + * Must equal SCENE_DETAILS_SCHEMA_VERSION on the Python side + * (`ansys.visor.viewer.models.runtime.visor_scene_details`). Bumped whenever + * the scene-details wire shape changes incompatibly. See proposal ยง2. + */ +export const SCENE_DETAILS_SCHEMA_VERSION = 2; + +/** + * Thrown when the payload's `schemaVersion` does not match the client's + * compiled-in `SCENE_DETAILS_SCHEMA_VERSION`. Distinguishable from every + * other parse-time error so `App.tsx` can catch this case, and only this + * case. See proposal ยง2.2. + */ +export class SchemaVersionMismatchError extends Error { + constructor(payloadVersion: number, expectedVersion: number) { + super( + `Scene details schema version mismatch: payload is version ${payloadVersion}, ` + + `client expects version ${expectedVersion}.` + ); + this.name = `SchemaVersionMismatchError`; + this.payloadVersion = payloadVersion; + this.expectedVersion = expectedVersion; + } + + readonly payloadVersion: number; + readonly expectedVersion: number; +} + +export default class VisorSceneDetails { + constructor(json_or_object: StateInput = null) { + const obj = + typeof json_or_object === `string` ? JSON.parse(json_or_object) : json_or_object; + // First statement, ahead of every sub-parse: see proposal ยง2.1. + ensureNumber(obj.schemaVersion, `schemaVersion`); + if (obj.schemaVersion !== SCENE_DETAILS_SCHEMA_VERSION) { + throw new SchemaVersionMismatchError(obj.schemaVersion, SCENE_DETAILS_SCHEMA_VERSION); + } + this.schemaVersion = obj.schemaVersion; + this.appState = new VisorAppState(obj.appState); + this.vtkInfo = new VisorVtkInfo(obj.vtkInfo); + } + + schemaVersion: number; + appState: VisorAppState; + vtkInfo: VisorVtkInfo; +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorSceneState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneState.tsx new file mode 100644 index 00000000..9c5f019d --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneState.tsx @@ -0,0 +1,283 @@ +import VisorDatasetState from './VisorDatasetState.tsx'; +import { + ensureBoolean, + ensureString, + JsonDict, + parseState, + StateInput, +} from './VisorStateCommon.tsx'; +import VisorSpectrumState from './VisorSpectrumState.tsx'; +import VisorCameraState from './VisorCameraState.tsx'; +import VisorCrossSectionState from './VisorCrossSectionState.tsx'; + +export default class VisorSceneState { + private _unit: string | undefined = undefined; + private _camera: VisorCameraState = new VisorCameraState(); + private _crossSection: VisorCrossSectionState = new VisorCrossSectionState(); + private _datasetStates: Record = {}; + private _spectrumStates: Record = {}; + private _orthographicEnabled: boolean | undefined = undefined; + private _crossSectionEnabled: boolean | undefined = undefined; + private _edgesEnabled: boolean | undefined = undefined; + private _boundingBoxEnabled: boolean | undefined = undefined; + + constructor(state: StateInput = null) { + this.copy(state, false); + } + + get camera(): VisorCameraState { + return this._camera; + } + + get crossSection(): VisorCrossSectionState { + return this._crossSection; + } + + get unit(): string | undefined { + return this._unit; + } + + setUnit(val: string | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._unit = val; + } + } else { + ensureString(val, 'val'); + this._unit = val; + } + } + + getDatasetState(idStr: string): VisorDatasetState | null { + ensureString(idStr, 'idStr'); + return this._datasetStates[idStr] ?? null; + } + + getDatasetStates(): VisorDatasetState[] { + return Object.values(this._datasetStates); + } + + copyDataset( + datasetState: StateInput, + replace = false, + key: string | number | null | undefined = null + ): VisorDatasetState | null { + if (datasetState == null) { + return null; + } + + const newState = new VisorDatasetState(datasetState, key); + + let existing; + if (newState.keyedByName) { + existing = this._datasetStates[newState.name]; + } else { + existing = this._datasetStates[newState.id]; + } + + if (existing) { + return existing.copy(newState, replace); + } + if (newState.keyedByName) { + return (this._datasetStates[newState.name] = newState); + } + return (this._datasetStates[newState.id] = newState); + } + + removeDataset(idStr: string): boolean { + return delete this._datasetStates[idStr]; + } + + getSpectrumState(idStr: string): VisorSpectrumState | null { + ensureString(idStr, 'idStr'); + return this._spectrumStates[idStr] ?? null; + } + + getSpectrumStates(): VisorSpectrumState[] { + return Object.values(this._spectrumStates); + } + + removeSpectrum(idStr: string): boolean { + return delete this._spectrumStates[idStr]; + } + + copySpectrum( + spectrumState: StateInput, + replace = false, + key: string | number | null | undefined = null + ): VisorSpectrumState | null { + if (spectrumState == null) { + return null; + } + + const newState = new VisorSpectrumState(spectrumState, key); + const existing = this._spectrumStates[newState.id]; + + if (existing) { + return existing.copy(newState, replace); + } + + this._spectrumStates[newState.id] = newState; + return newState; + } + + get orthographicEnabled(): boolean | undefined { + return this._orthographicEnabled; + } + + setOrthographicEnabled(val: boolean | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._orthographicEnabled = val; + } + } else { + ensureBoolean(val, 'val'); + this._orthographicEnabled = val; + } + } + + get crossSectionEnabled(): boolean | undefined { + return this._crossSectionEnabled; + } + + setCrossSectionEnabled(val: boolean | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._crossSectionEnabled = val; + } + } else { + ensureBoolean(val, 'val'); + this._crossSectionEnabled = val; + } + } + + get edgesEnabled(): boolean | undefined { + return this._edgesEnabled; + } + + setEdgesEnabled(val: boolean | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._edgesEnabled = val; + } + } else { + ensureBoolean(val, 'val'); + this._edgesEnabled = val; + } + } + + get boundingBoxEnabled(): boolean | undefined { + return this._boundingBoxEnabled; + } + + setBoundingBoxEnabled(val: boolean | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._boundingBoxEnabled = val; + } + } else { + ensureBoolean(val, 'val'); + this._boundingBoxEnabled = val; + } + } + + copy(state: StateInput = null, replace = false): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorSceneState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + this._camera.copy(data.camera, replace); + + this._crossSection.copy(data.crossSection, replace); + + this.setUnit(data.unit === undefined ? this._unit : data.unit, replace); + this.setCrossSectionEnabled( + data.crossSectionEnabled === undefined + ? this._crossSectionEnabled + : data.crossSectionEnabled, + replace + ); + this.setBoundingBoxEnabled( + data.boundingBoxEnabled === undefined + ? this._boundingBoxEnabled + : data.boundingBoxEnabled, + replace + ); + this.setEdgesEnabled( + data.edgesEnabled === undefined ? this._edgesEnabled : data.edgesEnabled, + replace + ); + this.setOrthographicEnabled( + data.orthographicEnabled === undefined + ? this._orthographicEnabled + : data.orthographicEnabled, + replace + ); + + const datasetStates = data.datasetStates; + if (datasetStates) { + for (const [k, v] of Object.entries(datasetStates)) { + this.copyDataset(v as JsonDict, replace, k); + } + + if (replace) { + for (const k of Object.keys(this._datasetStates)) { + if (!datasetStates[k]) { + delete this._datasetStates[k]; + } + } + } + } + + const spectrumStates = data.spectrumStates; + if (spectrumStates) { + for (const [k, v] of Object.entries(spectrumStates)) { + this.copySpectrum(v as JsonDict, replace, k); + } + if (replace) { + for (const k of Object.keys(this._spectrumStates)) { + if (!spectrumStates[k]) { + delete this._spectrumStates[k]; + } + } + } + } + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + const spectrumStates: JsonDict = {}; + for (const [k, v] of Object.entries(this._spectrumStates)) { + if (k === v.id) { + spectrumStates[k] = v.toDict(); + } + } + const datasetStates: JsonDict = {}; + for (const [k, v] of Object.entries(this._datasetStates)) { + if ((v.keyedByName && k === v.name) || k === v.id) { + datasetStates[k] = v.toDict(); + } + } + return { + unit: this.unit, + camera: this.camera.toDict(), + crossSection: this.crossSection.toDict(), + orthographicEnabled: this.orthographicEnabled, + crossSectionEnabled: this.crossSectionEnabled, + edgesEnabled: this.edgesEnabled, + boundingBoxEnabled: this.boundingBoxEnabled, + spectrumStates, + datasetStates, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx new file mode 100644 index 00000000..a9eecd08 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx @@ -0,0 +1,106 @@ +import { + ensureArray, + ensureNumberArray, + JsonDict, + parseState, + StateInput, +} from './VisorStateCommon.tsx'; + +export default class VisorSpectrumState { + private _id: string = ''; + private _magnitudeRange: number[] | undefined = undefined; + private _ranges: (number[] | undefined)[] = []; + + constructor( + state: StateInput = null, + key: string | number | null | undefined = null + ) { + this.copy(state, false, key); + } + + get id(): string { + return this._id; + } + + setId(val: string | number | null | undefined): void { + if (val != null) { + this._id = val.toString(); + } + } + + get magnitudeRange(): number[] | undefined { + return this._magnitudeRange; + } + + setMagnitudeRange(val: number[] | undefined, replace = false): void { + if (val === undefined) { + if (replace) { + this._magnitudeRange = undefined; + } + } else { + ensureNumberArray(val, 'val', 2); + this._magnitudeRange = val; + } + } + + get ranges(): (number[] | undefined)[] { + return this._ranges; + } + + setRanges(val: (number[] | undefined)[], replace = false): void { + ensureArray(val, 'val'); + for (let i = 0; i < val.length; i++) { + const minmax = val[i]; + if (minmax == undefined) { + if (replace) { + this._ranges[i] = minmax; + } + } else { + ensureNumberArray(minmax, 'minmax', 2); + this._ranges[i] = minmax; + } + } + } + + copy( + state: StateInput = null, + replace = false, + key: string | number | null | undefined = null + ): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorSpectrumState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + const thisId = (data.id ?? this._id)?.toString(); + + if ((key = key?.toString()) != null && key !== thisId) { + throw new Error(`Spectrum state with id '${thisId}' does not equal key '${key}'`); + } + + this.setId(thisId); + this.setMagnitudeRange( + data.magnitudeRange === undefined ? this._magnitudeRange : data.magnitudeRange, + replace + ); + this.setRanges(data.ranges === undefined ? this._ranges : data.ranges, replace); + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + return { + id: this.id, + magnitudeRange: this.magnitudeRange, + ranges: this.ranges, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorStateCommon.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorStateCommon.tsx new file mode 100644 index 00000000..86c23cea --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorStateCommon.tsx @@ -0,0 +1,63 @@ +export type JsonDict = { [key: string]: any }; +export type StateInput = T | string | JsonDict | null; + +export function ensureNumberArray(val: any, name: string, ensureLen?: number): any[] { + ensureArray(val, name, ensureLen); + for (const num of val) { + ensureNumber(num, name); + } + return val; +} + +export function ensureArray(val: any, name: string, ensureLen?: number): any[] { + if (!Array.isArray(val)) { + throw new TypeError(`${name} must be an array`); + } + if (ensureLen != null && ensureLen >= 0 && val.length > ensureLen) { + throw new TypeError(`${name} must have exactly ${ensureLen} element(s)`); + } + return val; +} + +export function ensureString(val: any, name: string): string { + if (typeof val !== 'string') { + throw new TypeError(`${name} must be a string`); + } + return val; +} + +export function ensureStringOrNull(val: any, name: string): string { + if (val !== null && typeof val !== 'string') { + throw new TypeError(`${name} must be a string or null`); + } + return val; +} + +export function ensureBoolean(val: any, name: string): boolean { + if (typeof val !== 'boolean') { + throw new TypeError(`${name} must be a boolean`); + } + return val; +} + +export function ensureNumber(val: any, name: string): number { + if (typeof val !== 'number') { + throw new TypeError(`${name} must be a number`); + } + return val; +} + +export function parseState(state: StateInput): JsonDict | null { + if (state == null) return null; + if (typeof state === 'string') { + const obj = JSON.parse(state); + if (typeof obj !== 'object' || obj === null) { + throw new Error('JSON string must deserialize to object'); + } + return obj; + } + if (typeof state === 'object') { + return state as JsonDict; + } + throw new TypeError('State must be object, JSON string, or null'); +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorUiState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorUiState.tsx new file mode 100644 index 00000000..614fa330 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorUiState.tsx @@ -0,0 +1,150 @@ +import { + ensureBoolean, + ensureNumber, + JsonDict, + parseState, + StateInput, +} from './VisorStateCommon.tsx'; + +export default class VisorUiState { + private _darkTheme: boolean | undefined = undefined; + private _panelTopLeftPanelCollapsed: boolean | undefined = undefined; + private _panelTopRightPanelCollapsed: boolean | undefined = undefined; + private _panelTopRightLegendCollapsed: boolean | undefined = undefined; + private _panelTopRightTabIndex: number | undefined = undefined; + + constructor(state: StateInput = null) { + this.copy(state); + } + + get darkTheme(): boolean | undefined { + return this._darkTheme; + } + + setDarkTheme(val: boolean | undefined, replace = false): void { + if (val == undefined) { + if (replace) { + this._darkTheme = val; + } + } else { + ensureBoolean(val, 'val'); + this._darkTheme = val; + } + } + + get panelTopLeftPanelCollapsed(): boolean | undefined { + return this._panelTopLeftPanelCollapsed; + } + + setPanelTopLeftPanelCollapsed(val: boolean | undefined, replace = false): void { + if (val == undefined) { + if (replace) { + this._panelTopLeftPanelCollapsed = val; + } + } else { + ensureBoolean(val, 'val'); + this._panelTopLeftPanelCollapsed = val; + } + } + + get panelTopRightPanelCollapsed(): boolean | undefined { + return this._panelTopRightPanelCollapsed; + } + + setPanelTopRightPanelCollapsed(val: boolean | undefined, replace = false): void { + if (val == undefined) { + if (replace) { + this._panelTopRightPanelCollapsed = val; + } + } else { + ensureBoolean(val, 'val'); + this._panelTopRightPanelCollapsed = val; + } + } + + get panelTopRightLegendCollapsed(): boolean | undefined { + return this._panelTopRightLegendCollapsed; + } + + setPanelTopRightLegendCollapsed(val: boolean | undefined, replace = false): void { + if (val == undefined) { + if (replace) { + this._panelTopRightLegendCollapsed = val; + } + } else { + ensureBoolean(val, 'val'); + this._panelTopRightLegendCollapsed = val; + } + } + + get panelTopRightTabIndex(): number | undefined { + return this._panelTopRightTabIndex; + } + + setPanelTopRightTabIndex(val: number | undefined, replace = false): void { + if (val == undefined) { + if (replace) { + this._panelTopRightTabIndex = val; + } + } else { + ensureNumber(val, 'val'); + this._panelTopRightTabIndex = val; + } + } + + copy(state: StateInput = null, replace = false): this { + if (state != null) { + let data: JsonDict; + + if (state instanceof VisorUiState) { + data = state.toDict(); + } else { + data = parseState(state)!; + } + + this.setDarkTheme( + data.darkTheme === undefined ? this._darkTheme : data.darkTheme, + replace + ); + this.setPanelTopLeftPanelCollapsed( + data.panelTopLeftPanelCollapsed === undefined + ? this._panelTopLeftPanelCollapsed + : data.panelTopLeftPanelCollapsed, + replace + ); + this.setPanelTopRightPanelCollapsed( + data.panelTopRightPanelCollapsed === undefined + ? this._panelTopRightPanelCollapsed + : data.panelTopRightPanelCollapsed, + replace + ); + this.setPanelTopRightLegendCollapsed( + data.panelTopRightLegendCollapsed === undefined + ? this._panelTopRightLegendCollapsed + : data.panelTopRightLegendCollapsed, + replace + ); + this.setPanelTopRightTabIndex( + data.panelTopRightTabIndex === undefined + ? this._panelTopRightTabIndex + : data.panelTopRightTabIndex, + replace + ); + } + return this; + } + + serialize(): string { + return JSON.stringify(this.toDict()); + } + + toDict(): JsonDict { + return { + darkTheme: this.darkTheme, + panelTopLeftPanelCollapsed: this.panelTopLeftPanelCollapsed, + panelTopRightPanelCollapsed: this.panelTopRightPanelCollapsed, + panelTopRightLegendCollapsed: this.panelTopRightLegendCollapsed, + panelTopRightTabIndex: this.panelTopRightTabIndex, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkDataArray.tsx b/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkDataArray.tsx new file mode 100644 index 00000000..eac6f010 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkDataArray.tsx @@ -0,0 +1,82 @@ +import { ensureArray, ensureNumber, ensureString, StateInput } from './../VisorStateCommon.tsx'; + +/** + * Define types for VTK data array types: can be 'POINT' or 'CELL' + */ +const FIELD_ASSOCIATIONS = ['POINT', 'CELL'] as const; + +/** + * Define union type for VTK data array based on list in FIELD_ASSOCIATIONS. + */ +export type FieldAssociation = (typeof FIELD_ASSOCIATIONS)[number]; + +/** + * Validator determining whether a string is a valid FieldAssociation + */ +const isFieldAssociation = (v: string): v is FieldAssociation => + (FIELD_ASSOCIATIONS as readonly string[]).includes(v); + +/** + * Describes a data array associated with a VTK dataset. + * + * A data array identifies its scalar or vector type, component count, + * component ranges, and overall magnitude range. + */ +export default class VisorVtkDataArray { + /** + * Creates a VTK data-array description from serialized state. + * + * @param json_or_object - A JSON string, plain state object, or other + * supported state input containing the data-array properties. + */ + constructor(json_or_object: StateInput = null) { + const obj = + typeof json_or_object === `string` ? JSON.parse(json_or_object) : json_or_object; + ensureNumber(obj.indexForType, `indexForType`); + ensureString(obj.type, `type`); + ensureString(obj.name, `name`); + ensureNumber(obj.numComponents, `numComponents`); + ensureArray(obj.magnitudeRange, `magnitudeRange`); + ensureArray(obj.ranges, `ranges`); + this.indexForType = obj.indexForType; + if (!isFieldAssociation(obj.type)) + throw new Error( + `type must be one of ${FIELD_ASSOCIATIONS.join(', ')}, received ${JSON.stringify(obj.type)}` + ); + this.type = obj.type; + this.name = obj.name; + this.numComponents = obj.numComponents; + this.magnitudeRange = obj.magnitudeRange; + this.ranges = obj.ranges; + } + + /** + * Position of this array among arrays of the same type. + */ + indexForType: number; + + /** + * VTK data-array type or classification. + */ + type: FieldAssociation; + + /** + * Display name of the data array. + */ + name: string; + + /** + * Number of components stored for each tuple in the data array. + */ + numComponents: number; + + /** + * Minimum and maximum magnitude values for the data array. + */ + magnitudeRange: number[]; + + /** + * Minimum and maximum values for each component. + */ + ranges: number[][]; +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkInfo.tsx b/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkInfo.tsx new file mode 100644 index 00000000..681107b3 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkInfo.tsx @@ -0,0 +1,32 @@ +import { StateInput } from './../VisorStateCommon.tsx'; +import VisorVtkSceneNode from './VisorVtkSceneNode.tsx'; +import { RendererAnnotation, parseRendererAnnotation } from '../../../renderer/RendererAnnotation'; + +/** + * Contains VTK scene metadata and the root of the serialized scene graph. + */ +export default class VisorVtkInfo { + /** + * Creates VTK scene information from serialized state. + * + * @param json_or_object - A JSON string, plain state object, or other + * supported state input containing the VTK metadata and scene graph. + */ + constructor(json_or_object: StateInput = null) { + const obj = + typeof json_or_object === `string` ? JSON.parse(json_or_object) : json_or_object; + this.sceneGraph = new VisorVtkSceneNode(obj.sceneGraph); + this.rendererAnnotation = parseRendererAnnotation(obj.rendererAnnotation); + } + + /** + * Root node of the VTK scene graph. + */ + sceneGraph: VisorVtkSceneNode; + + /** + * Renderer-specific handles. `null` if the renderer has no client-side + * handles to hand the client. Consumed only by the matching IRenderer. + */ + rendererAnnotation: RendererAnnotation | null; +} diff --git a/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkSceneNode.tsx b/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkSceneNode.tsx new file mode 100644 index 00000000..00001493 --- /dev/null +++ b/src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkSceneNode.tsx @@ -0,0 +1,105 @@ +import { + ensureArray, + ensureBoolean, + ensureNumber, + ensureString, + StateInput, +} from './../VisorStateCommon.tsx'; +import VisorVtkDataArray from './VisorVtkDataArray.tsx'; + +/** + * Represents a node in a hierarchical VTK scene graph. + * + * A node may represent an actor, a logical group, or another supported scene + * element. Child nodes and associated data arrays are recursively converted + * into their corresponding class instances. + */ +export default class VisorVtkSceneNode { + /** + * Creates a scene-graph node from serialized state. + * + * @param json_or_object - A JSON string, plain state object, or other + * supported state input containing the node properties. + */ + constructor(json_or_object: StateInput = null) { + const obj = + typeof json_or_object === `string` ? JSON.parse(json_or_object) : json_or_object; + ensureNumber(obj.id, `id`); + ensureArray(obj.dataArrays, `dataArrays`); + ensureString(obj.name, `name`); + ensureBoolean(obj.isActorNode, `isActorNode`); + ensureBoolean(obj.isGroupNode, `isGroupNode`); + ensureString(obj.nodeType, `nodeType`); + ensureArray(obj.bounds, `bounds`); + ensureArray(obj.diffuseColor, `diffuseColor`); + ensureArray(obj.children, `children`); + this.id = obj.id; + this.name = obj.name; + this.isActorNode = obj.isActorNode; + this.isGroupNode = obj.isGroupNode; + this.nodeType = obj.nodeType; + + // obj.bounds should be a plain number array. + // I'm not going to number-type check each bounds + // array element as that could be quite slow on a + // large scene graph. - BHB 2026-01-28 + this.bounds = obj.bounds; + this.diffuseColor = obj.diffuseColor; + + this.children = new Array(obj.children.length); + for (let i = 0; i < obj.children.length; i++) { + this.children[i] = new VisorVtkSceneNode(obj.children[i]); + } + + this.dataArrays = new Array(obj.dataArrays.length); + for (let i = 0; i < obj.dataArrays.length; i++) { + this.dataArrays[i] = new VisorVtkDataArray(obj.dataArrays[i]); + } + } + + /** + * Application-level identifier for this scene node. + */ + id: number; + + /** + * Data arrays available on the dataset represented by this node. + */ + dataArrays: VisorVtkDataArray[]; + + /** + * Display name of the scene node. + */ + name: string; + + /** + * Indicates whether this node represents a VTK actor. + */ + isActorNode: boolean; + + /** + * Indicates whether this node is a logical grouping node. + */ + isGroupNode: boolean; + + /** + * String identifier describing the node's scene-graph type. + */ + nodeType: string; + + /** + * Axis-aligned bounds of the node, typically represented as + * `[xMin, xMax, yMin, yMax, zMin, zMax]`. + */ + bounds: number[]; + + /** + * Diffuse RGB color associated with the node. + */ + diffuseColor: number[]; + + /** + * Direct descendants of this scene-graph node. + */ + children: VisorVtkSceneNode[]; +} diff --git a/src/ansys/visor/visor-client/src/styles/index.css b/src/ansys/visor/visor-client/src/styles/index.css new file mode 100644 index 00000000..aad782cd --- /dev/null +++ b/src/ansys/visor/visor-client/src/styles/index.css @@ -0,0 +1,320 @@ +*, +*::before, +*::after { + margin: 0; + padding: 0; + line-height: 1; + text-align: left; + font-size: inherit; + box-sizing: border-box; + border-style: solid; + border-width: 0; + visibility: inherit; +} + +html { + height: 100%; +} + +@font-face { + /* + The "Source Sans 3" font is used by the Ansys AWC React library, + and was imported manually from there for use in Visor. + */ + font-family: 'Source Sans 3'; + src: url('/fonts/source-sans-3.woff2') format('woff2'); +} + +body { + height: 100%; + /* + This font list is used by the Ansys AWC React library, + and was imported manually from there for use in Visor. + This font list can be found in CSS file "@ansys\awc-react\styles\themes\css\default.css" + after installing the AWC React npm package. + */ + font-family: + 'Source Sans 3', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Noto Sans', + Ubuntu, + Cantarell, + 'Helvetica Neue', + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji'; +} + +a, +a:active, +a:link, +a:visited, +a:hover { + text-decoration: underline; + cursor: pointer; +} + +/************************/ +/*Floating Divs*/ +/************************/ + +.float-container-left, +.float-container-right { + &::before, + &::after { + /*clearfix*/ + content: ''; + display: block; + clear: both; + } + + & > * { + display: block; + position: relative; + width: auto; + /*include min-height:1px because floating + divs that take up no space are removed from + the document, and we don't want them to be + removed.*/ + min-height: 1px; + } +} + +.float-container-left > * { + float: left; +} + +.float-container-right > * { + float: right; +} + +.bold { + font-weight: bold; +} + +.hard-wrap { + white-space: normal; + word-break: break-all; +} + +.no-wrap { + white-space: nowrap; +} + +.text-right { + text-align: right; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +*, +*:active, +*:link, +*:visited, +*:hover { + &.no-decoration { + text-decoration: none; + } +} + +.no-select { + user-select: none; +} + +.nowrap { + white-space: nowrap; +} + +.border-all { + border-width: 1px; +} + +.margin-top { + margin-top: 10px; +} + +.margin-top-5 { + margin-top: 5px; +} + +.margin-right { + margin-right: 10px; +} + +.margin-right-5 { + margin-right: 5px; +} + +.padding-all { + padding: 10px; +} + +.padding-right { + padding-right: 10px; +} + +.padding-right-20 { + padding-right: 20px; +} + +.padding-bottom { + padding-bottom: 10px; +} + +:is(th, td).stretch { + width: 99.99%; +} + +:is(table, th, td).shrink { + width: 0.01px; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +div { + display: block; +} + +.right { + margin-left: auto; +} + +.center { + margin-left: auto; + margin-right: auto; +} + +.left { + margin-right: auto; +} + +label { + display: block; + + & > *:first-child { + &:not(input, textarea, select, button) { + font-size: 10px; + } + } + + & > *:not(:first-child) { + margin-top: 5px; + } + + & *:is(input, textarea, select, button) { + display: block; + } +} + +table.list > * > tr > *:first-child { + white-space: nowrap; + text-align: right; + width: 0.01px; +} + +table:is(.pad-h-5, .pad-h-10) { + & > * > tr > *:first-child { + padding-left: 0; + } + + & > * > tr > *:last-child { + padding-right: 0; + } +} + +table { + &.pad-h-5 > * > tr > * { + padding-left: 3px; + padding-right: 2px; + } + + &.pad-h-10 > * > tr > * { + padding-left: 5px; + padding-right: 5px; + } +} + +table:is(.pad-v-5, .pad-v-10) { + & > * > tr:first-child > * { + padding-top: 0; + } + + & > * > tr:last-child > * { + padding-bottom: 0; + } +} + +table { + &.pad-v-5 > * > tr > * { + padding-top: 3px; + padding-bottom: 2px; + } + + &.pad-v-10 > * > tr > * { + padding-top: 5px; + padding-bottom: 5px; + } +} + +table { + width: 100%; + empty-cells: show; + border-collapse: collapse; + border-spacing: 0; + table-layout: auto; + vertical-align: baseline; + display: table; +} + +tr { + display: table-row; +} + +th { + display: table-cell; + vertical-align: middle; +} + +td { + display: table-cell; + vertical-align: middle; +} + +table:is(.padh5, .padh10) { + & > * > tr > *:first-child { + padding-left: 0; + } + + & > * > tr > *:last-child { + padding-right: 0; + } +} + +table { + &.padh5 > * > tr > * { + padding-left: 3px; + padding-right: 2px; + } + + &.padh10 > * > tr > * { + padding-left: 5px; + padding-right: 5px; + } +} diff --git a/src/ansys/visor/visor-client/src/styles/inputs.css b/src/ansys/visor/visor-client/src/styles/inputs.css new file mode 100644 index 00000000..3da6d7e4 --- /dev/null +++ b/src/ansys/visor/visor-client/src/styles/inputs.css @@ -0,0 +1,176 @@ +textarea, +select, +input, +button { + font-family: inherit; + display: inline-block; + vertical-align: top; + + &:not(select) { + line-height: 1; + } + + &:disabled { + transform: none; + cursor: auto; + } + + &:focus-visible { + outline-offset: -2px; + outline: rgba(105, 187, 255, 1) solid 4px; + } +} + +button { + border-radius: 6px; + background-color: #4dabf5; + font-weight: bold; + padding: 10px; + text-align: center; + cursor: pointer; +} + +input[type='color'], +.color-input-height { + --height: 18px; + width: 100%; + + &:is(input) { + cursor: pointer; + border-radius: 0; + height: var(--height); + } + + &:not(input) { + min-height: var(--height); + line-height: var(--height); + display: block; + } +} + +input[type='range'], +.range-input-height { + --height: 18px; + width: 100%; + + &:is(input) { + height: var(--height); + } + + &:not(input) { + min-height: var(--height); + line-height: var(--height); + display: block; + } +} + +textarea, +input[type='text'], +input[type='password'], +.text-input-height { + /* the minimum line-height + * for text inputs is 1.125 */ + --line-height: 1.125; + --font-size: 16px; + --padding: 4px; + --border-width: 1px; + + &:is(textarea, input) { + line-height: var(--line-height); + font-size: var(--font-size); + padding: var(--padding); + border-width: var(--border-width); + border-radius: 3px; + width: 100%; + -webkit-appearance: none; + overflow-x: clip; + overflow-y: auto; + background-color: #ffffff; + border-color: rgba(0, 0, 0, 0.35); + text-align: left; + color: #000000; + + &:is(textarea) { + min-height: 7em; + resize: none; + white-space: pre-wrap; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + } + + &:not(:read-only) { + background-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.15) 0, + rgba(0, 0, 0, 0) 10px + ); + } + + &:nth-child(n) { + font-family: Arial, monospace; + } + } + + &:not(textarea, input) { + min-height: calc( + (var(--font-size) * var(--line-height)) + (var(--padding) * 2) + + (var(--border-width) * 2) + ); + line-height: calc( + (var(--font-size) * var(--line-height)) + (var(--padding) * 2) + + (var(--border-width) * 2) + ); + display: block; + } +} + +select, +.select-input-height { + --line-height: 20px; + --font-size: 12px; + --border-width: 1px; + --select-input-height: calc(var(--line-height) + (var(--border-width) * 2)); + + &:is(select) { + line-height: var(--line-height); + border-width: var(--border-width); + font-size: var(--font-size); + -webkit-appearance: none; + border-style: solid; + overflow: hidden; + width: auto; + min-width: max(100%, var(--select-input-height)); + text-align: left; + border-radius: 4px; + border-color: rgba(0, 0, 0, 0.35); + background-color: revert; + color: #000000; + background-size: + auto, + 100% 100%; + background-position: + right center, + top left; + background-repeat: no-repeat, repeat; + + &:enabled { + padding: 0 26px 0 5px; + cursor: pointer; + background-image: + url('data:image/svg+xml,'), + url('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs='); + } + + &:disabled { + padding: 0 5px 0 5px; + background-image: url('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs='); + } + } + + &:not(select) { + min-height: var(--select-input-height); + line-height: var(--select-input-height); + display: block; + } +} diff --git a/src/ansys/visor/visor-client/src/styles/themes/visor-theme-dark.css b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-dark.css new file mode 100644 index 00000000..330af855 --- /dev/null +++ b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-dark.css @@ -0,0 +1,11 @@ +:root { + --theme-color-1: 255, 255, 255; + --theme-color-2: 234, 234, 240; + --theme-color-3: 216, 216, 222; + --theme-color-borders: 121, 121, 129; + --theme-color-text: 53, 53, 53; + --theme-color-selected-background: 233, 244, 254; + --theme-color-selected-text: 33, 150, 243; + /* Visor-specific settings below */ + --visor-viewer-background: 26, 26, 26; +} diff --git a/src/ansys/visor/visor-client/src/styles/themes/visor-theme-light.css b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-light.css new file mode 100644 index 00000000..bf3fa07d --- /dev/null +++ b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-light.css @@ -0,0 +1,11 @@ +:root { + --theme-color-1: 255, 255, 255; + --theme-color-2: 234, 234, 240; + --theme-color-3: 216, 216, 222; + --theme-color-borders: 121, 121, 129; + --theme-color-text: 53, 53, 53; + --theme-color-selected-background: 233, 244, 254; + --theme-color-selected-text: 33, 150, 243; + /* Visor-specific settings below */ + --visor-viewer-background: 247, 247, 247; +} diff --git a/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css new file mode 100644 index 00000000..911acc5a --- /dev/null +++ b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css @@ -0,0 +1,87 @@ +*, +*::after, +*::before { + border-color: rgba(var(--theme-color-borders), 1); +} + +html, +body { + font-size: 14px; + color: rgba(var(--theme-color-text), 1); +} + +.visor-viewer-background { + background-color: rgba(var(--visor-viewer-background), 1); +} + +.theme-text-color { + color: rgba(var(--theme-color-text), 1); +} + +.theme-selected-background { + background-color: rgba(var(--theme-color-selected-background), 1); + fill: rgba(var(--theme-color-selected-background), 1); +} + +.theme-selected-text { + background-color: rgba(var(--theme-color-selected-text), 1); + fill: rgba(var(--theme-color-selected-text), 1); +} + +.theme-border-radius { + border-radius: 6px; + overflow: hidden; +} + +.theme-panel-1, +.theme-panel-2, +.theme-panel-3 { + box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.4); + border-radius: 6px; + border-width: 0; + padding: 10px; + overflow: auto; +} + +.theme-color-1 { + color: rgba(var(--theme-color-1), 1); +} + +.theme-color-2 { + color: rgba(var(--theme-color-2), 1); +} + +.theme-color-3 { + color: rgba(var(--theme-color-3), 1); +} + +.theme-panel-1, +.theme-background-1, +.theme-hover-background-1:hover { + background-color: rgba(var(--theme-color-1), 1); + fill: rgba(var(--theme-color-1), 1); +} + +.theme-panel-2, +.theme-background-2, +.theme-hover-background-2:hover { + background-color: rgba(var(--theme-color-2), 1); + fill: rgba(var(--theme-color-2), 1); +} + +.theme-panel-3, +.theme-background-3, +.theme-hover-background-3:hover { + background-color: rgba(var(--theme-color-3), 1); + fill: rgba(var(--theme-color-3), 1); +} + +label > *:first-child, +.theme-border-color { + color: rgba(var(--theme-color-borders), 1); +} + +.theme-legend-overlay { + background-color: rgba(0, 0, 0, var(--visor-legend-overlay-opacity)); + color: rgba(var(--visor-legend-overlay-text-color), 1); +} diff --git a/src/ansys/visor/visor-client/src/treeview/TreeView.tsx b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx new file mode 100644 index 00000000..44259f1e --- /dev/null +++ b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx @@ -0,0 +1,709 @@ +import { useEffect } from 'react'; +import { AwcIcons } from '../components/AwcIcons'; +import { escapeHtml, getRootFontSize, randomId, randomInt } from '../utils/JsHelpers'; + +/** + * Describes a node that can be rendered by {@link TreeView}. + * + * @template T The concrete recursive node type. + */ +export interface ITreeViewNode> { + /** Unique identifier used to index the corresponding tree row. */ + id: number; + /** Human-readable label displayed in the tree. */ + name: string; + /** Whether the node should currently be visible in the underlying scene. */ + visible: boolean; + /** Whether the node acts as a group that may contain child rows. */ + isGroupNode: boolean; + /** Child nodes rendered beneath this node. */ + children: T[]; +} + +/** + * Properties accepted by {@link TreeView}. + * + * @template T The concrete tree-node type. + */ +interface TreeViewProps> { + /** Root node of the scene graph to render. No tree is created when omitted. */ + sceneGraph?: T; + /** Called after the tree has been built and its utility API is ready. */ + onLoad?: (util: TreeViewUtil) => void; +} + +/** + * Runtime state and DOM handles associated with one rendered tree row. + * + * The object itself is read-only, while several properties expose mutable DOM + * elements and child arrays managed internally by the tree. + * + * @template T The concrete tree-node type. + */ +export type TreeViewRow> = Readonly<{ + /** Source node represented by this row. */ + node: T; + /** Outer element containing this row and its descendant row elements. */ + nodeElem: HTMLDivElement; + /** Table element that renders the interactive row. */ + tableElem: HTMLTableElement; + /** Parent row, or `null` for the root row. */ + parentUtil: TreeViewRow | null; + /** Returns whether `row` appears in this row's root-to-self lineage. */ + contains: (row: TreeViewRow) => boolean; + /** Cached lineage ordered from the root row through this row, inclusive. */ + lineage: TreeViewRow[]; + /** Whether this row's child container is expanded. */ + expanded: boolean; + /** Whether this row is currently marked visible. */ + visible: boolean; + /** Whether this row is currently selected. */ + selected: boolean; + /** Button used to expand a group row. */ + expandButton: HTMLButtonElement; + /** Button used to collapse a group row. */ + collapseButton: HTMLButtonElement; + /** Button used to mark the row visible. */ + showButton: HTMLButtonElement; + /** Button used to mark the row hidden. */ + hideButton: HTMLButtonElement; + /** Whether this row represents a group node. */ + isComposite: boolean; + /** Zero-based depth of the row, with the root at depth `0`. */ + depth: number; + /** Position in the tree's flattened pre-order row array. */ + index: number; + /** Direct child rows. */ + children: TreeViewRow[]; + /** Selects this row and optionally all descendants. */ + selectRow: (recursive?: boolean) => void; + /** Deselects this row and optionally all descendants. */ + deselectRow: (recursive?: boolean) => void; + /** Marks this row visible and updates its visibility control. */ + show: () => void; + /** Marks this row hidden and updates its visibility control. */ + hide: () => void; +}>; + +/** + * Imperative utility API exposed after a {@link TreeView} has loaded. + * + * @template T The concrete tree-node type. + */ +export type TreeViewUtil> = Readonly<{ + /** Total number of rows in the flattened tree. */ + nodeCount: number; + /** Flattened pre-order collection of all row utilities. */ + rows: TreeViewRow[]; + /** Lookup table from node ID to its row utility. */ + rowUtilsMap: Map>; + /** + * Registers a selection-change listener. + * + * @param handler Callback receiving the current selected-node array. + * @returns A function that unregisters the listener. + */ + addSelectionChangeListener: (handler: (selectedNodes: T[]) => void) => () => void; + /** + * Registers a visibility-change listener. + * + * @param handler Callback receiving the current visible-node array. + * @returns A function that unregisters the listener. + */ + addVisibilityChangeListener: (handler: (visibleNodes: T[]) => void) => () => void; + /** + * Rebuilds the selected-node array and reconciles group selection state. + * + * @param startingUtil Optional subtree root used for group-state reconciliation. + */ + updateSelectedNodesArray: (startingUtil?: TreeViewRow | null) => void; + /** Mutable array containing the currently selected source nodes. */ + selectedNodes: T[]; + /** Mutable array containing the currently visible source nodes. */ + visibleNodes: T[]; + /** + * Replaces the current selection with rows matching the supplied node IDs. + * + * @param selectedNodeIds Node IDs to select. + * @param runEventListeners Whether to notify registered listeners; defaults to `true`. + */ + setSelection: (selectedNodeIds: number[], runEventListeners?: boolean) => void; + /** Synchronizes row visibility from each source node's `visible` property. */ + synchronize: () => void; +}>; + +/** + * Renders an imperative, searchable tree view for a recursive scene graph. + * + * Selection supports standard click, Ctrl-click, and Shift-click behavior. + * Visibility controls can affect either one row, all selected rows, or a group + * row and all of its descendants. + * + * @template T The concrete tree-node type. + * @param props Tree configuration and optional load callback. + * @returns The container element populated by the effect after mounting. + */ +export const TreeView = >(props: TreeViewProps) => { + const { sceneGraph, onLoad = () => undefined } = props; + const componentContainerElemId = randomId(); + useEffect(createTreeElem, []); + return
; + + /** + * Builds the tree DOM, initializes row state, and exposes the utility API. + * + * @returns A React effect cleanup callback, or `undefined` when no graph exists. + */ + function createTreeElem() { + if (sceneGraph == null) { + return; + } + const fontSize: number = getRootFontSize(14); + const componentContainerElem = document.getElementById( + componentContainerElemId + ) as HTMLDivElement; + const headerContainer = document.createElement('div'); + const treeContainer = document.createElement('div'); + componentContainerElem.appendChild(headerContainer); + componentContainerElem.appendChild(treeContainer); + const selectedNodes: T[] = []; + const visibleNodes: T[] = []; + const rowUtilsMap: Map> = new Map(); + const rowUtilsArr: TreeViewRow[] = []; + const selectionChangeListeners: Record void> = {}; + const visibilityChangeListeners: Record void> = {}; + (() => { + // placing these variable declarations inside an + // IIFE so we don't pollute the outer scope + const rootRow = createRowUtil(sceneGraph, 0, null); + treeContainer.appendChild(rootRow.nodeElem); + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = 'Search...'; + searchInput.oninput = (e) => { + if (searchInput.value === '') { + rowUtilsArr.forEach((row) => { + row.tableElem.style.removeProperty('display'); + }); + return; + } + const lower = searchInput.value.toLowerCase(); + rowUtilsArr.forEach((row) => { + if (row.node.name.toLowerCase().includes(lower)) { + row.tableElem.style.removeProperty('display'); + } else { + row.tableElem.style.display = 'none'; + } + }); + }; + const headerMarkup = getNodeMarkup(0); + headerMarkup.nodeElem.style.marginBottom = '5px'; + headerMarkup.cells[1].appendChild(searchInput); + headerContainer.appendChild(headerMarkup.nodeElem); + // BHB 2025-06-19: per standup meeting on 2025-06-19 we are not + // adding a "global" show/hide button for selected actors. + // Showing/hiding all selected actors is done by clicking on the + // show/hide button for any selected actor in the tree view. + // addSelectedShowHideButton(); + + /** + * Adds the currently disabled global visibility control for selected rows. + * + * @remarks The caller is intentionally commented out per the dated note above. + */ + function addSelectedShowHideButton() { + const showButton = AwcIcons.getHideIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + const hideButton = AwcIcons.getShowIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + showButton.onclick = showSelected; + hideButton.onclick = hideSelected; + headerMarkup.cells[2].appendChild(showButton); + headerMarkup.cells[2].appendChild(hideButton); + showSelected(null); + + /** + * Marks all selected rows visible and emits a visibility-change event. + * + * @param e Optional originating mouse event. + */ + function showSelected(e: MouseEvent | null) { + showButton.remove(); + headerMarkup.cells[2].appendChild(hideButton); + rowUtilsArr.forEach((u) => { + u.selected && u.show(); + }); + runVisibilityChangeListeners(visibleNodes); + e?.stopImmediatePropagation(); + } + + /** + * Marks all selected rows hidden and emits a visibility-change event. + * + * @param e Optional originating mouse event. + */ + function hideSelected(e: MouseEvent | null) { + hideButton.remove(); + headerMarkup.cells[2].appendChild(showButton); + rowUtilsArr.forEach((u) => { + u.selected && u.hide(); + }); + runVisibilityChangeListeners(visibleNodes); + e?.stopImmediatePropagation(); + } + } + })(); + onLoad({ + nodeCount: rowUtilsArr.length, + rows: rowUtilsArr, + rowUtilsMap, + addSelectionChangeListener: (handler) => { + const key = randomInt(); + selectionChangeListeners[key] = handler; + return () => delete selectionChangeListeners[key]; + }, + addVisibilityChangeListener: (handler) => { + const key = randomInt(); + visibilityChangeListeners[key] = handler; + return () => delete visibilityChangeListeners[key]; + }, + updateSelectedNodesArray, + selectedNodes, + visibleNodes, + setSelection, + synchronize, + }); + return () => { + // no useEffect() cleanup required + }; + + /** + * Replaces the current selection using node IDs. + * + * @param selectedNodeIds IDs of rows to select. + * @param runEventListeners Whether registered selection listeners should run. + */ + function setSelection(selectedNodeIds: number[], runEventListeners: boolean = true) { + rowUtilsArr.forEach((u) => u.deselectRow()); + for (const id of selectedNodeIds) { + const rowUtil = rowUtilsMap.get(id); + rowUtil != null && rowUtil.selectRow(); + } + updateSelectedNodesArray(null); + if (runEventListeners) { + runSelectionChangeListeners(selectedNodes); + } + } + + /** Synchronizes every row's visibility from its source node. */ + function synchronize() { + rowUtilsArr.forEach((u) => { + u.node.visible ? u.show() : u.hide(); + }); + updateVisibleNodesArray(null); + } + + /** + * Invokes all registered selection-change listeners. + * + * @param selectedNodes Current selected source nodes. + */ + function runSelectionChangeListeners(selectedNodes: T[]) { + Object.values(selectionChangeListeners).forEach((handler) => { + handler(selectedNodes); + }); + } + + /** + * Invokes all registered visibility-change listeners. + * + * @param visibleNodes Current visible source nodes. + */ + function runVisibilityChangeListeners(visibleNodes: T[]) { + Object.values(visibilityChangeListeners).forEach((handler) => { + handler(visibleNodes); + }); + } + + /** + * Creates the base DOM structure for a tree row or header row. + * + * @param depth Zero-based indentation depth. + * @returns The outer element, table element, and first-row cells. + */ + function getNodeMarkup(depth: number) { + const nodeElem = document.createElement('div'); + let h = ``; + h += ``; + h += ``; + h += ``; + h += ``; + h += ``; + h += ``; + h += `
`; + nodeElem.innerHTML = h; + const tableElem = nodeElem.getElementsByTagName('table')[0]; + tableElem.style.fontSize = `${fontSize}px`; + tableElem.style.borderColor = 'transparent'; + tableElem.style.borderWidth = '2px 0'; + tableElem.style.userSelect = 'none'; + const cells = tableElem.rows[0].cells; + cells[0].style.paddingLeft = `${fontSize * depth}px`; + return { + nodeElem, + tableElem, + cells, + }; + } + + /** + * Rebuilds `selectedNodes` and selects a group only when all descendants are selected. + * + * @param startingUtil Optional subtree root for group-state reconciliation. + */ + function updateSelectedNodesArray(startingUtil?: TreeViewRow | null) { + selectedNodes.length = 0; + startingUtil ??= rowUtilsArr[0]; + allDescendantsSelected(startingUtil); + rowUtilsArr.forEach((u) => { + u.selected && selectedNodes.push(u.node); + }); + + /** + * Reconciles selection state for one row and its descendants. + * + * @param util Row whose subtree should be evaluated. + * @returns `true` when the row or all of its descendants are selected. + */ + function allDescendantsSelected(util: TreeViewRow) { + if (util.isComposite) { + let result = true; + for (const childUtil of util.children) { + if (!allDescendantsSelected(childUtil)) { + result = false; + } + } + if (!result) { + util.deselectRow(); + } else { + util.selectRow(); + } + return result; + } + return util.selected; + } + } + + /** + * Rebuilds `visibleNodes` and derives group visibility from descendant visibility. + * + * @param startingUtil Optional subtree root for visibility reconciliation. + */ + function updateVisibleNodesArray(startingUtil?: TreeViewRow | null) { + visibleNodes.length = 0; + allDescendantsVisible(startingUtil ?? rowUtilsArr[0]); + rowUtilsArr.forEach((u) => { + u.visible && visibleNodes.push(u.node); + }); + + /** + * Reconciles visibility state for one row and its descendants. + * + * @param util Row whose subtree should be evaluated. + * @returns `true` when the row or at least one descendant is visible. + */ + function allDescendantsVisible(util: TreeViewRow) { + if (util.isComposite) { + let result = false; + for (const childUtil of util.children) { + if (allDescendantsVisible(childUtil)) { + result = true; + } + } + if (!result) { + util.hide(); + } else { + util.show(); + } + return result; + } + return util.visible; + } + } + + /** + * Recursively creates a rendered row and its runtime utility object. + * + * @param node Source node represented by the row. + * @param depth Zero-based depth within the tree. + * @param parentUtil Parent row utility, or `null` for the root. + * @returns The initialized row utility. + */ + function createRowUtil(node: T, depth: number, parentUtil: TreeViewRow | null) { + const { nodeElem, tableElem, cells } = getNodeMarkup(depth); + cells[1].innerHTML = escapeHtml(node.name); + const childrenDiv = document.createElement('div'); + nodeElem.appendChild(childrenDiv); + tableElem.onclick = tableClick; + let selected: boolean = false; + let visible: boolean = false; + const showButton = AwcIcons.getHideIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + const hideButton = AwcIcons.getShowIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + const getLineage: () => TreeViewRow[] = (() => { + const lineage: TreeViewRow[] = []; + return () => { + if (lineage.length === 0) { + let curUtil: TreeViewRow | null = rowUtil; + while (curUtil != null) { + lineage.push(curUtil); + curUtil = curUtil.parentUtil; + } + lineage.reverse(); + Object.freeze(lineage); + } + return lineage; + }; + })(); + + let expanded = false; + let expandButton: HTMLButtonElement = null!; + let collapseButton: HTMLButtonElement = null!; + if (node.isGroupNode) { + expandButton = AwcIcons.getArrowHeadRightIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + collapseButton = AwcIcons.getArrowHeadDownIcon(fontSize * 1.2, 3, true, [ + 'theme-hover-background-3', + ]) as HTMLButtonElement; + expandButton.onclick = (e) => { + expand(); + e.stopPropagation(); + }; + collapseButton.onclick = (e) => { + collapse(); + e.stopPropagation(); + }; + expand(); + + /** Expands the group row and reveals its child container. */ + function expand() { + expandButton!.remove(); + cells[0].appendChild(collapseButton!); + childrenDiv.style.removeProperty('display'); + expanded = true; + } + + /** Collapses the group row and hides its child container. */ + function collapse() { + collapseButton!.remove(); + cells[0].appendChild(expandButton!); + childrenDiv.style.display = 'none'; + expanded = false; + } + } else { + // cells[0].appendChild(AwcIcons.getNullIcon(0)); + } + const rowUtil: TreeViewRow = { + node, + nodeElem, + tableElem, + parentUtil, + isComposite: node.isGroupNode, + depth, + index: rowUtilsArr.length, + children: [], + contains: (row) => { + return getLineage().includes(row); + }, + get lineage() { + return getLineage(); + }, + get expanded() { + return expanded; + }, + get visible() { + return visible; + }, + get selected() { + return selected; + }, + expandButton, + collapseButton, + showButton, + hideButton, + show() { + showButton.remove(); + cells[2].appendChild(hideButton); + visible = true; + }, + hide() { + hideButton.remove(); + cells[2].appendChild(showButton); + visible = false; + }, + selectRow: (recursive) => { + if (selected && !recursive) { + return; + } + tableElem.classList.add('theme-selected-background'); + selected = true; + if (recursive === true) { + for (const child of rowUtil.children) { + child.selectRow(recursive); + } + } + }, + deselectRow: (recursive) => { + if (!selected && !recursive) { + return; + } + tableElem.classList.remove('theme-selected-background'); + selected = false; + if (recursive === true) { + for (const child of rowUtil.children) { + child.deselectRow(recursive); + } + } + }, + }; + rowUtilsMap.set(node.id, rowUtil); + showButton.onclick = (e) => { + if (selected) { + rowUtilsArr.forEach((u) => { + u.selected && u.show(); + }); + } else { + rowUtil.show(); + if (rowUtil.isComposite) { + for (let i = rowUtil.index + 1; i < rowUtilsArr.length; i++) { + const u = rowUtilsArr[i]; + if (u.depth <= rowUtil.depth) { + break; + } + u.show(); + } + } + } + updateVisibleNodesArray(rowUtilsArr[0]); + runVisibilityChangeListeners(visibleNodes); + e.stopImmediatePropagation(); + }; + hideButton.onclick = (e) => { + if (selected) { + rowUtilsArr.forEach((u) => { + u.selected && u.hide(); + }); + } else { + rowUtil.hide(); + if (rowUtil.isComposite) { + for (let i = rowUtil.index + 1; i < rowUtilsArr.length; i++) { + const u = rowUtilsArr[i]; + if (u.depth <= rowUtil.depth) { + break; + } + u.hide(); + } + } + } + updateVisibleNodesArray(rowUtilsArr[0]); + runVisibilityChangeListeners(visibleNodes); + e.stopImmediatePropagation(); + }; + rowUtil.show(); + rowUtilsArr.push(rowUtil); + cells[1].style.paddingLeft = `${fontSize / 4}px`; + depth++; + for (const childNode of node.children) { + const childUtil = createRowUtil(childNode, depth, rowUtil); + rowUtil.children.push(childUtil); + childrenDiv.appendChild(childUtil.nodeElem); + } + return rowUtil; + + /** + * Applies click, Ctrl-click, or Shift-click selection behavior for this row. + * + * @param e Mouse event raised by the row table. + */ + function tableClick(e: MouseEvent) { + if (e.ctrlKey && e.shiftKey) { + return; + } else if (e.shiftKey) { + let startIndex = 0; + let endIndex = 0; + for (let i = rowUtil.index + 1; i < rowUtilsArr.length; i++) { + if (rowUtilsArr[i].selected) { + for (let c = i + 1; c < rowUtilsArr.length; c++) { + if (!rowUtilsArr[c].selected) { + break; + } + i = c; + } + endIndex = i + 1; + break; + } + } + if (endIndex > 0) { + startIndex = rowUtil.index; + } else { + endIndex = rowUtil.index + 1; + for (let i = rowUtil.index - 1; i >= 0; i--) { + if (rowUtilsArr[i].selected) { + for (let c = i - 1; c >= 0; c--) { + if (!rowUtilsArr[c].selected) { + break; + } + i = c; + } + startIndex = i; + break; + } + } + } + for (let i = 0; i < startIndex; i++) { + rowUtilsArr[i].deselectRow(); + } + for (let i = startIndex; i < endIndex; i++) { + const u = rowUtilsArr[i]; + if (i === endIndex - 1 && u.isComposite) { + u.selectRow(true); + let newEndIndex = i + 1 + u.children.length; + for (let c = newEndIndex; c < rowUtilsArr.length; c++) { + if (rowUtilsArr[c].depth <= u.depth) { + newEndIndex = c; + break; + } + } + endIndex = newEndIndex; + } else { + u.selectRow(); + } + } + for (let i = endIndex; i < rowUtilsArr.length; i++) { + rowUtilsArr[i].deselectRow(); + } + updateSelectedNodesArray(null); + runSelectionChangeListeners(selectedNodes); + return; + } else if (!e.ctrlKey) { + rowUtilsArr.forEach((u) => { + u.deselectRow(); + }); + } + if (selected) { + rowUtil.deselectRow(true); + } else { + rowUtil.selectRow(true); + } + updateSelectedNodesArray(null); + runSelectionChangeListeners(selectedNodes); + } + } + } +}; diff --git a/src/ansys/visor/visor-client/src/utils/JsHelpers.js b/src/ansys/visor/visor-client/src/utils/JsHelpers.js new file mode 100644 index 00000000..9b0c3a44 --- /dev/null +++ b/src/ansys/visor/visor-client/src/utils/JsHelpers.js @@ -0,0 +1,400 @@ +/** @type {number|null} */ +let rootFontSize = null; + +/** + * Returns the root element's computed font size in pixels. + * + * If the computed font size is not expressed in pixels, the supplied fallback + * value is returned. The computed value, or the failure to obtain one, is + * cached for subsequent calls. + * + * @param {number} fallback - Value to return when the root font size cannot be + * determined in pixels. Must be zero or greater. + * @returns {number} The root font size in pixels, or the fallback value. + * @throws {Error} If `fallback` is not a non-negative number. + */ +export const getRootFontSize = (fallback) => { + if (typeof fallback !== 'number' || fallback < 0) { + throw new Error('fallback must be a number 0 or greater'); + } + + if (rootFontSize == null) { + const str = getComputedStyle(document.documentElement, null).getPropertyValue('font-size'); + + if (/^(?:-?\d+|-?\d*[.,]\d+)px$/i.test(str)) { + return (rootFontSize = parseFloat(str.slice(0, -2))); + } + + const msg = `TreeView warning: computed root element font size '${str}'`; + + console.warn(`${msg} is not a pixel value. Using fallback.`); + rootFontSize = -1; + } + + if (rootFontSize < 0) { + return fallback; + } + + return rootFontSize; +}; + +/** @type {number} */ +const MIN_ID = 1_000_000_000_000_000; + +/** @type {number} */ +const MAX_ID = Number.MAX_SAFE_INTEGER; + +/** @type {number} */ +const RANGE = MAX_ID - MIN_ID + 1; + +/** + * Generates a deterministic numeric ID from a string. + * + * The resulting value is between {@link MIN_ID} and {@link MAX_ID}, inclusive. + * + * @param {string} str - String to hash. + * @returns {number} A deterministic safe integer derived from the string. + */ +export const hashString = (str) => { + const h = cyrb53(str); + return MIN_ID + (h % RANGE); +}; + +/** + * Produces a 53-bit hash from a string using the cyrb53 algorithm. + * + * @param {string} str - String to hash. + * @param {number} [seed=0] - Optional numeric seed. + * @returns {number} A non-negative 53-bit integer. + */ +function cyrb53(str, seed = 0) { + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + + for (let i = 0; i < str.length; i++) { + const ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (h2 & 2097151) + (h1 >>> 0); +} + +/** + * Generates a random integer between `min` and `max`, inclusive. + * + * By default, `min` is 10^15 and `max` is JavaScript's maximum safe integer, + * 9007199254740991. This ensures that the generated number is exactly 16 + * digits long, which is useful when displaying IDs in a fixed-width table + * column. + * + * @param {number} [min=1000000000000000] - Minimum possible value. + * @param {number} [max=Number.MAX_SAFE_INTEGER] - Maximum possible value. + * @returns {number} A random integer between `min` and `max`, inclusive. + */ +export function randomInt(min = 1000000000000000, max = Number.MAX_SAFE_INTEGER) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +/** + * Generates a random HTML element ID. + * + * @returns {string} An underscore-prefixed random numeric ID. + */ +export function randomId() { + return `_${randomInt()}`; +} + +/** + * Rounds a number to a specified number of decimal places. + * + * Based on https://stackoverflow.com/a/48764436. + * + * @param {number} input - Number to round. + * @param {number} [places=0] - Number of decimal places. + * @returns {number} The rounded number. + */ +export function roundFloat(input, places) { + const p = Math.pow(10, places || 0); + const n = input * p * (1 + Number.EPSILON); + return Math.round(n) / p; +} + +/** @type {RegExp} */ +const reScale = /scale\([^)]+\)|none/gi; + +/** + * Sets the CSS scale transform of an HTML element. + * + * Any existing `scale(...)` transform or `none` value is removed before the + * new scale is prepended. Other transform functions are preserved. + * + * @param {HTMLElement} elem - Element whose transform should be updated. + * @param {number} scale - Horizontal and vertical scale factor. + * @returns {void} + */ +export const setScale = (elem, scale) => { + const style = elem.style; + style.transform = `scale(${scale},${scale}) ${style.transform.replace(reScale, '')}`; +}; + +/** @type {RegExp} */ +const regex_RemoveFloor = /^\d*\.?/gi; + +/** @type {RegExp} */ +const regex_AddCommas = /(?<=^[^.]+)(?=(?:\d{3})+(?:$|\.))/gi; + +/** + * Adds thousands separators to the integer portion of a numeric string. + * + * @param {string} str - Numeric string to format. + * @returns {string} The string with thousands separators added. + */ +const addCommas = (str) => str.replace(regex_AddCommas, ','); + +/** + * Formats a number with optional rounding, decimal padding, and thousands + * separators. + * + * @param {number} num - Number to format. + * @param {number} round - Number of decimal places. Pass a negative value to + * disable rounding. + * @param {boolean} pad - Whether to pad the fractional part with trailing + * zeroes. + * @returns {string} The formatted number. + */ +export const formatNumber = (num, round, pad) => { + if (round > -1) { + num = roundFloat(num, round); + } + + const str = num.toString(); + + if (round > 0 && pad) { + const repeat = round - str.replace(regex_RemoveFloor, '').length; + + if (repeat > 0) { + const dot = repeat === round ? '.' : ''; + return addCommas(str) + dot + '0'.repeat(repeat); + } + } + + return addCommas(str); +}; + +/** + * Escapes characters with special meaning in HTML. + * + * Null and undefined inputs are treated as empty strings. + * + * @param {string|null|undefined} input - String to escape. + * @returns {string} The HTML-escaped string. + */ +export function escapeHtml(input) { + input ??= ''; + input = input.replace(/&/g, '&'); + input = input.replace(//g, '>'); + input = input.replace(/"/g, '"'); + input = input.replace(/'/g, '''); + return input; +} + +/** + * Copies all own property descriptors from a source object to a target object. + * + * Unlike `Object.assign()` and the object spread operator, this function copies + * accessor properties themselves rather than copying only their current return + * values. + * + * When `seal` is true, copied descriptors are made non-configurable and copied + * data properties are made non-writable. + * + * Based on: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#copying_accessors + * + * @template {object} T + * @param {T} target - Object that will receive the source properties. + * @param {boolean} seal - Whether copied descriptors should be made immutable. + * @param {object} source - Object whose own property descriptors will be copied. + * @returns {T} The modified target object. + * @throws {TypeError} If `target` or `source` is null or undefined. + * @throws {TypeError} If `seal` is not a boolean. + * @public + */ +export function completeAssign(target, seal, source) { + if (target == null) { + throw new TypeError('target cannot be null'); + } else if (typeof seal !== 'boolean') { + throw new TypeError('seal must be a boolean or null'); + } else if (source == null) { + throw new TypeError('source cannot be null'); + } + + const descriptors = Object.getOwnPropertyDescriptors(source); + + if (seal) { + for (const key of Reflect.ownKeys(descriptors)) { + const d = descriptors[key]; + d.configurable = false; + + if ('writable' in d) { + d.writable = false; + } + } + } + + Object.defineProperties(target, descriptors); + return target; +} + +/** + * Wraps an object factory in a lazy-loading shell. + * + * The factory is invoked the first time the shell's `value` property is read. + * Subsequent reads return the cached value. The value cannot be assigned + * directly, and recursive access from within the factory throws an error. + * + * @template T + * @param {() => T} factory - Function that creates the lazily loaded value. + * @returns {{readonly value: T}} A shell exposing the cached value. + * @throws {Error} If the value is accessed recursively from within its factory. + * @throws {Error} If an attempt is made to assign to the `value` property. + * @public + */ +export function lazy(factory) { + /** @type {boolean} */ + let isSet = false; + + /** @type {boolean} */ + let setting = false; + + /** @type {T|null} */ + let obj = null; + + return { + get value() { + if (isSet) { + return /** @type {T} */ (obj); + } else if (setting) { + throw new Error('cannot access a lazy value from within its factory'); + } + + setting = true; + + try { + obj = factory(); + } finally { + setting = false; + } + + isSet = true; + return obj; + }, + + set value(x) { + throw new Error('the value property is readonly'); + }, + }; +} + +/** + * Creates a promise together with an externally accessible resolver function. + * + * The returned `isSet` function reports whether the resolver has been called. + * + * @template T + * @returns {{ + * isSet: () => boolean, + * promise: Promise, + * resolver: (value: T | PromiseLike) => void + * }} The promise, its resolver, and a function reporting its resolution state. + */ +export function getPromiseResolver() { + /** @type {(value: T|PromiseLike) => void} */ + let resolver; + + /** @type {boolean} */ + let isSet = false; + + const promise = new Promise((resolve) => { + resolver = function (value) { + isSet = true; + resolve(value); + }; + }); + + return { + isSet() { + return isSet; + }, + promise, + resolver, + }; +} + +/** + * Retrieves a cookie value by name. + * + * The cookie name is escaped for use in a regular expression and then + * URI-encoded before the cookie string is searched. + * + * @param {string|null|undefined} key - Cookie name. + * @returns {string} The matching encoded cookie value, or the original cookie + * string when no match is found. + */ +export function getCookie(key) { + // Escape key for regex. + key = key?.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&') ?? ''; + + // Encode key for cookie. + key = encodeURIComponent(key); + + const re = new RegExp(`^(?:.*;\\s*)?${key}=([^;]*).*$`, 'gsi'); + return document.cookie.replace(re, '$1'); +} + +/** + * Determines whether a value is a number or a numeric string. + * + * Numeric strings may contain an optional leading minus sign and either a + * period or comma as the decimal separator. + * + * @param {unknown} val - Value to inspect. + * @returns {boolean} Whether the value is numeric. + */ +export function isNumeric(val) { + if (typeof val === 'number') { + return true; + } else if (typeof val !== 'string') { + return false; + } + + return /^-?\d+$|^-?\d*[.,]\d+$/.test(val); +} + +/** + * Attempts to convert a string to a floating-point number. + * + * Numbers, null, and undefined are returned unchanged. A string that cannot be + * parsed returns null. All other input types cause an error. + * + * @param {string|number|null|undefined} input - Value to parse. + * @returns {number|null|undefined} The parsed number, the original number, + * null, or undefined. + * @throws {Error} If the input is not a string, number, null, or undefined. + */ +export function tryParseFloat(input) { + if (typeof input === 'string') { + const num = parseFloat(input); + return isNaN(num) ? null : num; + } else if (typeof input === 'number' || input == null) { + return input; + } + + throw new Error('input must be a string, number, null, or undefined'); +} diff --git a/src/ansys/visor/visor-client/src/utils/PrivateConstructor.js b/src/ansys/visor/visor-client/src/utils/PrivateConstructor.js new file mode 100644 index 00000000..e6e83f6f --- /dev/null +++ b/src/ansys/visor/visor-client/src/utils/PrivateConstructor.js @@ -0,0 +1,18 @@ +/** + * Abstract base class that cannot be instantiated directly. + * + * @abstract + */ +class PrivateConstructor { + /** + * Creates an instance of {@link PrivateConstructor}. + * + * @throws {Error} Always throws because the constructor is private. + * @protected + */ + constructor() { + throw new Error('constructor is private'); + } +} + +export default PrivateConstructor; diff --git a/src/ansys/visor/visor-client/src/utils/Tooltip.tsx b/src/ansys/visor/visor-client/src/utils/Tooltip.tsx new file mode 100644 index 00000000..6a78f81e --- /dev/null +++ b/src/ansys/visor/visor-client/src/utils/Tooltip.tsx @@ -0,0 +1,98 @@ +/** + * A unique property key used to mark elements that already have a tooltip. + * + * The randomized key prevents collisions with other properties attached to + * the target element. + */ +const skipKey = `_${crypto.randomUUID()}`; + +/** + * Adds a tooltip to an HTML element. + * + * The tooltip is displayed when the target is hovered or focused and hidden + * when the pointer leaves the target or the target loses focus. Calling this + * function more than once for the same element has no additional effect. + * + * @param target - The HTML element to which the tooltip will be attached. + * @param text - The text displayed inside the tooltip. + * @returns A cleanup function that removes the tooltip event listeners. + * + * @example + * ```ts + * const button = document.querySelector('#save'); + * + * if (button) { + * const disposeTooltip = makeTooltip(button, 'Save changes'); + * + * // Remove the tooltip listeners when they are no longer needed. + * disposeTooltip(); + * } + * ``` + */ +export function makeTooltip(target: HTMLElement, text: string): () => void { + if ((target as any)[skipKey] === true) { + // if the tooltip has already been added, return early + return () => undefined; + } + + (target as any)[skipKey] = true; + + const tip = document.createElement('div'); + tip.className = 'visor-tooltip'; + tip.textContent = text; + tip.classList.add('theme-background-1'); + + // Inline minimal styling to avoid depending on CSS files + const style = tip.style; + style.position = 'absolute'; + style.padding = '6px 8px'; + style.borderWidth = '1px'; + style.borderRadius = '4px'; + style.fontSize = '12px'; + style.pointerEvents = 'none'; + style.transform = 'translateY(-8px)'; + style.transition = 'opacity 0.12s ease, transform 0.12s ease'; + style.opacity = '0'; + style.zIndex = '10000'; + style.whiteSpace = 'nowrap'; + + document.body.appendChild(tip); + + /** + * Positions and displays the tooltip. + * + * @param e - The mouse-enter or focus event that triggered the tooltip. + */ + const onEnter = (e: Event): void => { + const r = target.getBoundingClientRect(); + const left = Math.round(r.left + r.width / 2 - tip.offsetWidth / 2); + const top = Math.round(r.top - tip.offsetHeight - 8); + + tip.style.left = `${Math.max(8, left)}px`; + tip.style.top = `${Math.max(8, top)}px`; + tip.style.opacity = '1'; + tip.style.transform = 'translateY(0px)'; + }; + + /** + * Hides the tooltip. + * + * @param e - The mouse-leave or blur event that triggered the tooltip. + */ + const onLeave = (e: Event): void => { + tip.style.opacity = '0'; + tip.style.transform = 'translateY(-8px)'; + }; + + target.addEventListener('mouseenter', onEnter); + target.addEventListener('focus', onEnter); + target.addEventListener('mouseleave', onLeave); + target.addEventListener('blur', onLeave); + + return () => { + target.removeEventListener('mouseenter', onEnter); + target.removeEventListener('focus', onEnter); + target.removeEventListener('mouseleave', onLeave); + target.removeEventListener('blur', onLeave); + }; +} diff --git a/src/ansys/visor/visor-client/src/utils/VisorColor.tsx b/src/ansys/visor/visor-client/src/utils/VisorColor.tsx new file mode 100644 index 00000000..1b89b195 --- /dev/null +++ b/src/ansys/visor/visor-client/src/utils/VisorColor.tsx @@ -0,0 +1,192 @@ +/** + * Represents an RGB color with hexadecimal, integer RGB, and normalized RGB + * representations. + * + * @example + * const color = new VisorColor() + * .setHex("#03f"); + * + * console.log(color.hex); + * console.log(color.rgb); + * console.log(color.rgbNormalized); + */ +class VisorColor { + /** + * Converts a hexadecimal color string to its RGB components. + * + * Supports both six-digit hexadecimal colors, such as `#0033ff`, and + * three-digit shorthand colors, such as `#03f`. + * + * @param hex - The hexadecimal color string to convert. + * @returns The red, green, and blue components in the range 0โ€“255, or + * `null` when the input is not a valid hexadecimal color. + */ + static #hexToRgb: (hex: string) => number[] | null; + + static { + const re1 = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + const re2 = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; + + /** + * Expands each component of a three-digit hexadecimal color. + * + * @param m - The full regular-expression match. + * @param r - The red hexadecimal digit. + * @param g - The green hexadecimal digit. + * @param b - The blue hexadecimal digit. + * @returns The expanded six-digit hexadecimal value without a leading + * hash. + */ + function replacer(m: string, r: string, g: string, b: string): string { + return r + r + g + g + b + b; + } + + this.#hexToRgb = (hex: string) => { + // A version of hexToRgb() that also parses a shorthand hex triplet + // such as "#03F". + // From https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + hex = (hex ?? '').replace(re1, replacer); + const result = re2.exec(hex); + + return result == null + ? null + : [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]; + }; + } + + /** + * Converts integer RGB components to a hexadecimal color string. + * + * @param r - The red component in the range 0โ€“255. + * @param g - The green component in the range 0โ€“255. + * @param b - The blue component in the range 0โ€“255. + * @returns A six-digit, lowercase hexadecimal color string. + * + * @throws {Error} If any component is outside the range 0โ€“255. + */ + static #rgbToHex(r: number, g: number, b: number): string { + if (r < 0 || r > 255) { + throw new Error(`r must be between 0 and 255`); + } else if (g < 0 || g > 255) { + throw new Error(`g must be between 0 and 255`); + } else if (b < 0 || b > 255) { + throw new Error(`b must be between 0 and 255`); + } + + return '#' + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1); + } + + /** The current hexadecimal color representation. */ + #hex: string; + + /** The current RGB components, each in the range 0โ€“255. */ + #rgb: Readonly; + + /** The current normalized RGB components, each in the range 0โ€“1. */ + #rgbNormalized: Readonly; + + /** + * Creates a color initialized to white. + */ + constructor() { + this.#hex = '#ffffff'; + this.#rgb = Object.freeze([255, 255, 255]); + this.#rgbNormalized = Object.freeze([1, 1, 1]); + } + + /** + * Gets the hexadecimal representation of the color. + * + * @returns The hexadecimal color string. + */ + get hex(): string { + return this.#hex; + } + + /** + * Gets the integer RGB representation of the color. + * + * @returns A read-only array containing the red, green, and blue + * components in the range 0โ€“255. + */ + get rgb(): Readonly { + return this.#rgb; + } + + /** + * Gets the normalized RGB representation of the color. + * + * @returns A read-only array containing the red, green, and blue + * components in the range 0โ€“1. + */ + get rgbNormalized(): Readonly { + return this.#rgbNormalized; + } + + /** + * Sets the color from a hexadecimal color string. + * + * Supports both six-digit hexadecimal colors, such as `#0033ff`, and + * three-digit shorthand colors, such as `#03f`. + * + * @param hex - The hexadecimal color string. + * @returns This color instance for method chaining. + * + * @throws {Error} If `hex` is not a valid hexadecimal color string. + */ + setHex(hex: string): this { + const temp = VisorColor.#hexToRgb(hex); + + if (temp == null) { + throw new Error(`invalid hex string: ${hex}`); + } + + this.#hex = hex; + this.#rgb = Object.freeze(temp); + this.#rgbNormalized = Object.freeze([ + this.#rgb[0] / 255, + this.#rgb[1] / 255, + this.#rgb[2] / 255, + ]); + + return this; + } + + /** + * Sets the color from RGB components. + * + * When `normalized` is `true`, each component is expected to be in the + * range 0โ€“1. Otherwise, each component is expected to be in the range + * 0โ€“255. + * + * @param r - The red component. + * @param g - The green component. + * @param b - The blue component. + * @param normalized - Whether the supplied components are normalized to + * the range 0โ€“1. + * @returns This color instance for method chaining. + * + * @throws {Error} If a converted RGB component is outside the range 0โ€“255. + */ + setRgb(r: number, g: number, b: number, normalized: boolean = true): this { + // rgbToHex() will throw an error if the color is invalid, + // so no need to validate arguments here. + if (normalized === true) { + const _r = Math.floor(r * 255); + const _g = Math.floor(g * 255); + const _b = Math.floor(b * 255); + + this.#hex = VisorColor.#rgbToHex(_r, _g, _b); + this.#rgb = Object.freeze([_r, _g, _b]); + this.#rgbNormalized = Object.freeze([r, g, b]); + } else { + this.#hex = VisorColor.#rgbToHex(r, g, b); + this.#rgb = Object.freeze([r, g, b]); + this.#rgbNormalized = Object.freeze([r / 255, g / 255, b / 255]); + } + + return this; + } +} + +export default VisorColor; diff --git a/src/ansys/visor/visor-client/src/vite-env.d.ts b/src/ansys/visor/visor-client/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/ansys/visor/visor-client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/ansys/visor/visor-client/src/wasm/RemoteVtkScene.js b/src/ansys/visor/visor-client/src/wasm/RemoteVtkScene.js new file mode 100644 index 00000000..84dd322f --- /dev/null +++ b/src/ansys/visor/visor-client/src/wasm/RemoteVtkScene.js @@ -0,0 +1,448 @@ +import { VtkWASMHandler } from '@kitware/vtk-wasm/remote'; +import TrameInterface from './TrameInterface.js'; +import VtkScene from './VtkScene.js'; +import { VtkWasmPerfCounter } from './VtkWasmPerfCounters.js'; + +/** + * @typedef {string|number|boolean|null|Object|{observe:()=>number}|RecordPromise)>} FrontendVtkObject + */ + +/** + * @desc `RemoteVtkScene` represents a VTK WASM scene that is synchronized with + * another VTK scene running on a Trame server. + */ +export default class RemoteVtkScene { + /** @type{HTMLDivElement} */ + static #invisibleDiv; + static { + const div = document.createElement('div'); + div.setAttribute('data-comments', 'invisible div for vtk trame connections'); + div.style.width = '0'; + div.style.height = '0'; + div.style.opacity = '0'; + div.style.overflow = 'hidden'; + div.style.position = 'absolute'; + div.style.inset = '0 auto auto 0'; + document.body.appendChild(div); + this.#invisibleDiv = div; + } + + /** + * @return {HTMLDivElement} + */ + static #getCanvasDiv = () => { + const canvasDiv = document.createElement('div'); + canvasDiv.setAttribute('data-comments', 'vtk canvas container'); + canvasDiv.style.width = '100%'; + canvasDiv.style.height = '100%'; + canvasDiv.style.boxSizing = 'border-box'; + canvasDiv.style.margin = '0'; + canvasDiv.style.border = '0'; + canvasDiv.style.padding = '0'; + canvasDiv.style.position = 'absolute'; + canvasDiv.style.inset = 'auto'; + canvasDiv.style.overflow = 'hidden'; + this.#invisibleDiv.appendChild(canvasDiv); + return canvasDiv; + }; + + /** + * @param {TrameSession} trameSession + * @param {string} wasmUrl + * @param {boolean} perfLogging + * @return {{ + * wasmHandler: VtkWASMHandler|any|Object|Record, + * vtkWasmPerfCounter: ?VtkWasmPerfCounter + * }} + */ + static #getWasmHandlerAsync = async (trameSession, wasmUrl, perfLogging) => { + const wasmHandler = new VtkWASMHandler(); + + /** @type {?VtkWasmPerfCounter} */ + let vtkWasmPerfCounter = null; + + if (perfLogging) { + vtkWasmPerfCounter = new VtkWasmPerfCounter(); + } + + async function netFetchState(vtkId) { + const res = await trameSession.call('vtklocal.get.state', [vtkId]); + vtkWasmPerfCounter?.recordStateFetch(res); + return res; + } + + async function netFetchBlob(hash) { + const t0 = perfLogging ? performance.now() : 0; + const res = await trameSession.call('vtklocal.get.hash', [hash]); + vtkWasmPerfCounter?.recordBlobFetch(hash, res, performance.now() - t0); + return res; + } + + async function netFetchStatus(vtkId) { + const t0 = perfLogging ? performance.now() : 0; + const res = await trameSession.call('vtklocal.get.status', [vtkId]); + vtkWasmPerfCounter?.logStatusFetch(vtkId, performance.now() - t0); + return res; + } + + wasmHandler.bindNetwork(netFetchState, netFetchBlob, netFetchStatus); + await wasmHandler.load(wasmUrl, { + print: (msg) => { + console.log(`Message from WebAssembly module: ${msg}`); + }, + printErr: (msg) => { + const prefix = `Error in WebAssembly module`; + if (this.#wasmUpdating) { + console.warn(`${prefix} during VtkWASMHandler.update(): ${msg}`); + } else if (this.#wasmGettingObject) { + console.warn(`${prefix} during VtkWASMHandler.getVtkObject(): ${msg}`); + } + console.warn(`${prefix}: ${msg}`); + }, + }); + return { wasmHandler, vtkWasmPerfCounter }; + }; + + /** + * @param {{ + * wasmIdsStateKey:string, + * refNameStateKey:string, + * webSocketUrl:string, + * wasmUrl:string, + * }} config + * @returns {Promise} + */ + static getInstanceAsync = async (config) => { + if (config == null) { + throw new Error(`config cannot be null`); + } else if (typeof config.wasmIdsStateKey !== 'string') { + throw new Error(`config.wasmIdsStateKey must be a string`); + } else if (typeof config.refNameStateKey !== 'string') { + throw new Error(`config.refNameStateKey must be a string`); + } else if (typeof config.webSocketUrl !== 'string') { + throw new Error(`config.webSocketUrl must be a string`); + } else if (typeof config.wasmUrl !== 'string') { + throw new Error(`config.wasmUrl must be a string`); + } + const { refNameStateKey, wasmIdsStateKey, webSocketUrl } = config; + let wasmUrl = config.wasmUrl; + const trameInterface = await TrameInterface.getInstanceAsync(webSocketUrl, refNameStateKey); + + // trame-vtklocal registers wasm files under a versioned path and + // exposes it as `__trame_vtklocal_wasm_url` in Trame state. + // We derive the absolute wasm base URL from the WebSocket origin + + // that state value so we always fetch from the server that actually + // has the files, regardless of which port the Dash app runs on. + const trameWasmUrl = trameInterface.getState('__trame_vtklocal_wasm_url'); + if (trameWasmUrl && typeof trameWasmUrl === 'string') { + // webSocketUrl is e.g. "ws://127.0.0.1:44473/ws" + // We need "http://127.0.0.1:44473/__trame_vtklocal/wasm/9.6.1" + const wsOrigin = webSocketUrl.replace(/^ws(s?):\/\//, 'http$1://').replace(/\/ws$/, ''); + wasmUrl = `${wsOrigin}/${trameWasmUrl}`; + } + + const wasmIds = trameInterface.getState(wasmIdsStateKey); + if (wasmIds == null) { + throw new Error(`wasmIds cannot be null`); + } else if (typeof wasmIds.rendererId !== 'number') { + throw new Error(`wasmIds.rendererId must be a number`); + } else if (typeof wasmIds.renderWindowId !== 'number') { + throw new Error(`wasmIds.renderWindowId must be a number`); + } + const rendererId = wasmIds.rendererId; + const renderWindowId = wasmIds.renderWindowId; + const perfLogging = trameInterface.getState('perf_logging') === true; + const { wasmHandler, vtkWasmPerfCounter } = await this.#getWasmHandlerAsync( + trameInterface.session, + wasmUrl, + perfLogging + ); + const canvasDiv = this.#getCanvasDiv(); + wasmHandler.bindCanvasToDOM(renderWindowId, canvasDiv); + const canvas = canvasDiv.getElementsByTagName('canvas')[0]; + canvas.style.cssText = `position:absolute;left:0;top:0;width:100%;height:100%;`; + + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + + this.#allowConstruction = true; + /**@type{RemoteVtkScene}*/ + let instance; + try { + instance = new RemoteVtkScene(); + } finally { + this.#allowConstruction = false; + } + instance.#wasmHandler = wasmHandler; + instance.#trameInterface = trameInterface; + instance.#perfCounter = vtkWasmPerfCounter; + instance.canvasDiv = canvasDiv; + instance.canvas = canvas; + instance.rendererId = rendererId; + instance.renderWindowId = renderWindowId; + // An initial call to updateAsync() is required for + // cameraIds to populate, getVtkObject() to work, etc. + await instance.updateAsync(true); + instance.renderer = instance.getVtkObject(rendererId); + instance.renderWindow = instance.getVtkObject(renderWindowId); + + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + + // Allow server to call method on us (update, resetCamera, evalStateExtract) + trameInterface.addSingleCallback('checkMemory', instance.#checkMemory); + trameInterface.addSingleCallback('evalStateExtract', instance.#evalStateExtract); + trameInterface.addSingleCallback('startEventLoop', instance.#startEventLoop); + trameInterface.addSingleCallback('stopEventLoop', instance.#stopEventLoop); + trameInterface.addSingleCallback('update', instance.updateAsync); + trameInterface.addSingleCallback('getState', (payload) => { + for (const callback of instance.#getStateListeners.values()) { + callback(payload); + } + }); + trameInterface.addSingleCallback('setState', (payload) => { + for (const callback of instance.#setStateListeners.values()) { + callback(payload); + } + }); + + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + + instance.#startEventLoop(); + instance.vtkScene = await VtkScene.getInstanceAsync(null, instance); + Object.freeze(instance); + return instance; + }; + + /**@type{boolean}*/ + static #allowConstruction = false; + /**@type{boolean}*/ + static #wasmUpdating = false; + /**@type{boolean}*/ + static #wasmGettingObject = false; + + constructor() { + if (!RemoteVtkScene.#allowConstruction) { + const msg = `Constructor is private.`; + throw new Error(`${msg} Use getInstanceAsync().`); + } + } + + /** + * @return {void} + */ + #checkMemory = () => { + const cacheSize = 100000000; + this.#wasmHandler.freeMemory(cacheSize); + }; + /** + * @param {Record>} definition + * @return {Promise} + */ + #evalStateExtract = async (definition) => { + this.#wasmHandler.clearStateCache(); + const promises = []; + for (const [name, props] of Object.entries(definition)) { + const value = {}; + for (const [propName, statePath] of Object.entries(props)) { + value[propName] = this.#wasmHandler.getStateValue(statePath, true); + } + promises.push(this.#trameInterface.setStateAsync(name, value)); + } + this.#wasmHandler.clearStateCache(); + await Promise.all(promises); + }; + /** + * @return {boolean} + */ + #startEventLoop = () => { + return this.#wasmHandler.sceneManager.startEventLoop(this.renderWindowId); + }; + /** + * @return {boolean} + */ + #stopEventLoop = () => { + return this.#wasmHandler.sceneManager.stopEventLoop(this.renderWindowId); + }; + + /**@type{any|Object|Record}*/ + #wasmHandler; + /**@type{TrameInterface}*/ + #trameInterface; + /**@type{?VtkWasmPerfCounter}*/ + #perfCounter = null; + /**@type{HTMLCanvasElement}*/ + canvas; + /**@type{HTMLDivElement}*/ + canvasDiv; + /**@type{number}*/ + rendererId; + /**@type{number}*/ + renderWindowId; + /**@type{FrontendVtkObject}*/ + renderer; + /**@type{FrontendVtkObject}*/ + renderWindow; + /**@type{VtkScene}*/ + vtkScene; + + /** + * @return {boolean} + */ + render = () => { + return this.#wasmHandler.sceneManager.render(this.renderWindowId); + }; + /** + * @return {Promise} + */ + resizeAsync = async () => { + const { width, height } = this.canvasDiv.getBoundingClientRect(); + const w = Math.floor(width * window.devicePixelRatio + 0.5); + const h = Math.floor(height * window.devicePixelRatio + 0.5); + await this.#wasmHandler.setSize(this.renderWindowId, w, h); + }; + /** + * @param {number} wasmId + * @return {FrontendVtkObject} + */ + getVtkObject = (wasmId) => { + let object; + RemoteVtkScene.#wasmGettingObject = true; + try { + object = this.#wasmHandler.getVtkObject(wasmId); + } catch (e) { + const msg = `Error getting VTK object with wasmId ${wasmId}: ${e.message}`; + throw new Error(msg); + } finally { + RemoteVtkScene.#wasmGettingObject = false; + } + return object; + }; + /** + * @param {boolean} [bindCanvas=false] + * @return {Promise} + */ + updateAsync = async (bindCanvas) => { + if (!this.#wasmHandler.loaded) { + return; + } + + let t0 = 0; + let t1 = 0; + + if (this.#perfCounter != null) { + this.#perfCounter.reset(); + t0 = performance.now(); + } + + RemoteVtkScene.#wasmUpdating = true; + try { + await this.#wasmHandler.update(this.renderWindowId, bindCanvas); + } finally { + RemoteVtkScene.#wasmUpdating = false; + } + if (this.#perfCounter != null) { + t1 = performance.now(); + } + + this.#checkMemory(); + await this.resizeAsync(); + + if (this.#perfCounter != null) { + const report = this.#perfCounter.logUpdateAsyncPerf( + t1 - t0, // wasmMs + performance.now() - t1 // resizeMs + ); + this.trameTriggerAsync('perf_report_wasm', report).catch(() => {}); + } + + for (const callback of this.#serverUpdatedListeners.values()) { + callback(); + } + }; + /** + * @param {string} name + * @param {function(...any):any} f + */ + trameAddSingleCallback = (name, f) => { + this.#trameInterface.addSingleCallback(name, f); + }; + /** + * @param {string} key + * @return {TrameStateValue} + */ + trameGetState = (key) => { + return this.#trameInterface.getState(key); + }; + /** + * @param {string} key + * @param {any} val + * @return {Promise} + */ + trameSetStateAsync = async (key, val) => { + await this.#trameInterface.setStateAsync(key, val); + }; + /** + * @param {string} triggerName + * @param {...any} args + * @return {Promise} + */ + trameTriggerAsync = async (triggerName, ...args) => { + return await this.#trameInterface.triggerAsync(triggerName, ...args); + }; + + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////// + + /**@type{Mapvoid>}*/ + #userObserverRemovers = new Map(); + /**@type{Mapvoid>}*/ + #serverUpdatedListeners = new Map(); + /**@type{Mapvoid>}*/ + #getStateListeners = new Map(); + /**@type{Mapvoid>}*/ + #setStateListeners = new Map(); + /** + * @return {void} + */ + clearObserversAndEventListeners = () => { + for (const remover of this.#userObserverRemovers.values()) { + remover(); + } + this.#serverUpdatedListeners.clear(); + this.#getStateListeners.clear(); + this.#setStateListeners.clear(); + }; + /** + * @param {()=>void} handler + * @return {()=>void} + */ + addServerUpdatedListener = (handler) => { + const remover = () => this.#serverUpdatedListeners.delete(remover); + this.#serverUpdatedListeners.set(remover, handler); + return remover; + }; + /** + * @param {(payload:any)=>void} handler + * @return {()=>void} + */ + addGetStateListener = (handler) => { + const remover = () => this.#getStateListeners.delete(remover); + this.#getStateListeners.set(remover, handler); + return remover; + }; + /** + * @param {(payload:any)=>void} handler + * @return {()=>void} + */ + addSetStateListener = (handler) => { + const remover = () => this.#setStateListeners.delete(remover); + this.#setStateListeners.set(remover, handler); + return remover; + }; +} diff --git a/src/ansys/visor/visor-client/src/wasm/TrameInterface.js b/src/ansys/visor/visor-client/src/wasm/TrameInterface.js new file mode 100644 index 00000000..20568e6d --- /dev/null +++ b/src/ansys/visor/visor-client/src/wasm/TrameInterface.js @@ -0,0 +1,160 @@ +import { Trame } from '@kitware/trame/src/trame.js'; + +/** + * @typedef {string|number|boolean|null|Object|Record} TrameStateValue + */ + +/** + * @name Trame#client + * @type {{getConnection:()=>TrameConnection}} + */ + +/** + * @typedef {{ + * connect:function():void, + * destroy:function(e):void, + * fireConnectionClose:function(u):void, + * fireConnectionError:function(u):void, + * fireConnectionReady:function(u):void, + * getSession:function():TrameSession, + * getUrl:function():string, + * isA:function(n):boolean, + * onConnectionClose:function(u):void, + * onConnectionError:function(u):void, + * onConnectionReady:function(u):void + * }} TrameConnection + */ + +/** + * @typedef {{ + * addAttachment:function(h:any):void, + * call:function(h:any,l:any,p2:any):Promise, + * close:function():void, + * destroy:function():void, + * isA:function(n:any):void, + * onconnect:function(h:any):void, + * onmessage:function(h:any):Promise, + * subscribe:function(h:any,l:any):void, + * unsubscribe:function(h:any):void + * }} TrameSession + */ + +/** + * @desc `TrameInterface` represents a connection to a Trame server. + * This class is simply a wrapper around a `Trame` object with the + * goal of simplifying communication with `Trame`. + */ +export default class TrameInterface { + static #allowConstruction = false; + /**@type{Trame}*/ + #trame; + /**@type{Object}*/ + #callbacksObject = {}; + + /** + * @param {string} s + * @return {boolean} + */ + static #stringIsInvalid(s) { + return typeof s !== 'string' || s.length === 0; + } + + constructor() { + if (!TrameInterface.#allowConstruction) { + const msg = `Constructor is private.`; + throw new Error(`${msg} Use getInstanceAsync().`); + } + } + + /** + * @param {string} webSocketUrl + * @param {string} refNameStateKey + * @return {Promise} + */ + static async getInstanceAsync(webSocketUrl, refNameStateKey) { + if (TrameInterface.#stringIsInvalid(refNameStateKey)) { + throw new Error(`refNameStateKey must be a non-empty string`); + } + const trame = new Trame(); + await trame.connect({ + application: 'trame', + sessionURL: webSocketUrl, + }); + const connection = trame.client.getConnection(); + + /**@type{TrameInterface}*/ + let instance; + this.#allowConstruction = true; + try { + instance = new TrameInterface(); + } finally { + this.#allowConstruction = false; + } + + instance.#trame = trame; + instance.session = connection.getSession(); + const refName = instance.getState(refNameStateKey); + if (TrameInterface.#stringIsInvalid(refName)) { + const msg = `The Trame state value obtained for '${refNameStateKey}' is invalid.`; + throw new Error(`${msg} '${refNameStateKey}' must be a non-empty string.`); + } + trame.refs[refName] = instance.#callbacksObject; + + return Object.freeze(instance); + } + + /**@type{TrameSession}*/ + session; + /** + * @param {Object} obj + */ + addCallbacks = (obj) => { + if (typeof obj !== 'object') { + throw new Error(`obj must be a non-function object`); + } + for (const [key, val] of Object.entries(obj)) { + if (typeof val === 'function') { + this.addSingleCallback(key, val); + } + } + }; + /** + * @param {string} name + * @param {function(...any):any} f + */ + addSingleCallback = (name, f) => { + if (typeof name !== 'string') { + throw new Error(`name must be a string`); + } else if (typeof f !== 'function') { + throw new Error(`f must be a function`); + } + const callbacksObject = this.#callbacksObject; + if (name in callbacksObject) { + throw new Error(`the property with name '${name}' has already been added`); + } + callbacksObject[name] = f; + }; + /** + * @param {string} key + * @return {TrameStateValue} + */ + getState = (key) => { + return this.#trame.state.get(key); + }; + /** + * @param {string} key + * @param {any} val + * @return {Promise} + */ + setStateAsync = async (key, val) => { + await this.#trame.state.set(key, val); + }; + /** + * @param {string} triggerName + * @param {...any} args + * @return {Promise} + */ + triggerAsync = async (triggerName, ...args) => { + return await this.#trame.trigger(triggerName, [...args]); + }; +} diff --git a/src/ansys/visor/visor-client/src/wasm/VtkScene.js b/src/ansys/visor/visor-client/src/wasm/VtkScene.js new file mode 100644 index 00000000..4449e3d6 --- /dev/null +++ b/src/ansys/visor/visor-client/src/wasm/VtkScene.js @@ -0,0 +1,541 @@ +/** + * @desc `VtkScene` represents an interface to a VTK WASM instance + * running locally in the browser. `VtkScene` can be initialized with + * a "standalone" VTK namespace, or a `RemoteVtkConnection` can be + * provided to the constructor to initialize the scene from an existing + * remote one. + */ +export default class VtkScene { + /** + * @private + */ + constructor() { + if (!VtkScene.#allowConstruction) { + const msg = `Constructor is private.`; + throw new Error(`${msg} Use getInstanceAsync().`); + } + } + + /**@type{boolean}*/ + static #allowConstruction = false; + + /** + * @param {?FrontendVtkObject?} vtk - Use a standalone VTK namespace for this scene. + * @param {?RemoteVtkScene?} remoteVtkScene - Base this scene off of an existing remote VTK scene. + */ + static getInstanceAsync = async (vtk, remoteVtkScene) => { + this.#allowConstruction = true; + /**@type{VtkScene}*/ + let instance; + try { + instance = new VtkScene(); + } finally { + this.#allowConstruction = false; + } + await this.#init.call(instance, vtk, remoteVtkScene); + Object.freeze(instance); + return instance; + }; + + /** + * @this {VtkScene} + * @param {?FrontendVtkObject?} vtk + * @param {?RemoteVtkScene?} remoteVtkScene + */ + static async #init(vtk, remoteVtkScene) { + /**@type{FrontendVtkObject}*/ + let camera; + /**@type{FrontendVtkObject}*/ + let interactor; + /**@type{FrontendVtkObject}*/ + let picker; + if (vtk != null) { + throw new Error('standalone sessions not yet supported'); + } else if (remoteVtkScene == null) { + throw new Error('if the VTK namespace is null, remoteVtkScene must be provided'); + } + const { renderer, renderWindow, canvasDiv, canvas } = remoteVtkScene; + camera = await renderer.GetActiveCamera(); + interactor = await renderWindow.GetInteractor(); + picker = await interactor.GetPicker(); + this.#remoteVtkScene = remoteVtkScene; + this.canvas = canvas; + this.canvasDiv = canvasDiv; + this.camera = camera; + this.renderer = renderer; + this.renderWindow = renderWindow; + this.interactor = interactor; + this.picker = picker; + this.#setupFpsMonitor(); + this.#setupCamera(); + this.#setupPicker(); + this.#setupHighlightOverlay(); + } + + /**@type{RemoteVtkScene}*/ + #remoteVtkScene; + /**@type{HTMLCanvasElement}*/ + canvas; + /**@type{HTMLDivElement}*/ + canvasDiv; + /** Last WASM display coords from mouseup โ€“ used by pickGeometryAsync. */ + #lastPickDisplayPos = null; + /**@type{FrontendVtkObject}*/ + camera; + /**@type{FrontendVtkObject}*/ + renderer; + /**@type{FrontendVtkObject}*/ + renderWindow; + /**@type{FrontendVtkObject}*/ + interactor; + /**@type{FrontendVtkObject}*/ + picker; + /** + * @param {number} wasmId + * @return {FrontendVtkObject} + */ + getVtkObject = (wasmId) => { + return this.#remoteVtkScene.getVtkObject(wasmId); + }; + getViewPortHeightAsync = async () => { + // renderWindow.GetSize() returns something like [300, 300] + const windowSize = await this.renderWindow.GetSize(); + const viewport = await this.renderer.GetViewport(); + return windowSize[1] * (viewport[3] - viewport[1]); + }; + /** + * @return {boolean} + */ + render = async () => { + return this.#remoteVtkScene.render(); + }; + /** + * @return {Promise} + */ + resizeAsync = async () => { + await this.#remoteVtkScene.resizeAsync(); + }; + /** + * @return {void} + */ + clearObserversAndEventListeners = () => { + for (const remover of this.#userObserverRemovers.values()) { + remover(); + } + this.#cameraChangedListeners.clear(); + this.#viewerClickedListeners.clear(); + this.#frameRenderedListeners.clear(); + }; + /**@type{Mapvoid>}*/ + #userObserverRemovers = new Map(); + /**@type{Mapvoid>}*/ + #viewerClickedListeners = new Map(); + /**@type{Mapvoid>}*/ + #cameraChangedListeners = new Map(); + /**@type{Mapvoid>}*/ + #frameRenderedListeners = new Map(); + /** + * @param {(cameraState:TrameStateValue)=>void} handler + * @return {()=>void} + */ + addCameraChangedListener = (handler) => { + const remover = () => this.#cameraChangedListeners.delete(remover); + this.#cameraChangedListeners.set(remover, handler); + return remover; + }; + /** + * @param {(actorId:number,ctrlKey:boolean,shiftKey:boolean,normX:number,normY:number)=>void} handler + * @return {()=>void} + */ + addViewerClickedListener = (handler) => { + const remover = () => this.#viewerClickedListeners.delete(remover); + this.#viewerClickedListeners.set(remover, handler); + return remover; + }; + /** + * Pick geometry at the last clicked position. + * + * The interactor's picker is a vtkCellPicker (set server-side), which + * returns the exact hit cell via GetCellId(). The actor's WASM object ID + * is retrieved from the actor proxy. Both are forwarded to the server so + * it can look up the cell vertices directly + * + * @param {number} normX - Normalized x [0..1] (kept for API compatibility) + * @param {number} normY - Normalized y [0..1] bottomโ†’top (VTK convention) + * @param {string} mode - 'vertex' | 'edge' | 'face' + * @return {Promise<{found:boolean, mode?:string, position?:number[], pointA?:number[], pointB?:number[], length?:number, area?:number, points?:number[][]}>} + */ + pickGeometryAsync = async (normX, normY, mode) => { + const displayPos = this.#lastPickDisplayPos; + if (!displayPos) return { found: false }; + const [dx, dy] = displayPos; + + // Re-pick at the exact WASM display coordinates. + const found = await this.picker.Pick([dx, dy, 0], this.renderer); + if (!found) return { found: false }; + + const rawPos = await this.picker.GetPickPosition(); + const position = [rawPos[0], rawPos[1], rawPos[2]]; + + // For all modes (vertex, edge, face): use vtkCellPicker's exact cell ID + // and actor WASM ID so the server can snap to the nearest vertex / edge / face. + try { + const cellId = await this.picker.GetCellId(); + if (cellId < 0) return { found: false }; + const actorProxy = await this.picker.GetActor(); + const actorWasmId = actorProxy?.id ?? null; + if (actorWasmId == null) return { found: false }; + return await this.#remoteVtkScene.trameTriggerAsync( + 'pick_geometry', + actorWasmId, + cellId, + mode, + position[0], + position[1], + position[2] + ); + } catch (_) { + return { found: false }; + } + }; + + // โ”€โ”€ Client-side geometry highlight (Canvas 2D overlay) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + /**@type{HTMLCanvasElement|null}*/ + #highlightCanvas = null; + /**@type{any|null}*/ + #highlightResult = null; + + #setupHighlightOverlay = () => { + const overlay = document.createElement('canvas'); + overlay.style.position = 'absolute'; + overlay.style.top = '0'; + overlay.style.left = '0'; + overlay.style.pointerEvents = 'none'; + this.canvasDiv.appendChild(overlay); + this.#highlightCanvas = overlay; + // Redraw when camera moves so the projected positions stay correct. + this.addCameraChangedListener(() => { + this.#drawHighlight().catch(() => {}); + }); + }; + + /** + * Project a world-space point to overlay canvas pixels (top-left origin). + * @param {number[]} worldPt [x, y, z] + * @return {Promise} [canvasX, canvasY] + */ + #projectWorldToCanvas = async (worldPt) => { + await this.renderer.SetWorldPoint(worldPt[0], worldPt[1], worldPt[2], 1.0); + await this.renderer.WorldToDisplay(); + const dp = await this.renderer.GetDisplayPoint(); + // dp: [x, y, z] โ€” VTK display coords (x leftโ†’right, y bottomโ†’top, in render-window pixels) + const winSize = await this.renderWindow.GetSize(); + const normX = dp[0] / winSize[0]; + const normY = dp[1] / winSize[1]; // 0 = bottom + const overlay = this.#highlightCanvas; + const canvasX = normX * overlay.width; + const canvasY = (1 - normY) * overlay.height; // flip Y for canvas2d + return [canvasX, canvasY]; + }; + + #drawHighlight = async () => { + const canvas = this.#highlightCanvas; + if (!canvas) return; + // Match canvas size to its CSS display size + const rect = this.canvasDiv.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const result = this.#highlightResult; + if (!result || !result.found) return; + + ctx.strokeStyle = 'rgba(255, 165, 0, 1)'; // orange + ctx.fillStyle = 'rgba(255, 165, 0, 0.35)'; + ctx.lineWidth = 3; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + if (result.mode === 'vertex') { + // Project the exact snapped vertex world position to canvas. + const [cx, cy] = await this.#projectWorldToCanvas(result.position); + ctx.beginPath(); + ctx.arc(cx, cy, 9, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + } else if (result.mode === 'edge') { + const [ax, ay] = await this.#projectWorldToCanvas(result.pointA); + const [bx, by] = await this.#projectWorldToCanvas(result.pointB); + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.moveTo(ax, ay); + ctx.lineTo(bx, by); + ctx.stroke(); + // endpoint dots + ctx.beginPath(); + ctx.arc(ax, ay, 5, 0, 2 * Math.PI); + ctx.fill(); + ctx.beginPath(); + ctx.arc(bx, by, 5, 0, 2 * Math.PI); + ctx.fill(); + } else if (result.mode === 'face' && result.points && result.points.length >= 3) { + const screenPts = []; + for (const p of result.points) { + screenPts.push(await this.#projectWorldToCanvas(p)); + } + ctx.beginPath(); + ctx.moveTo(screenPts[0][0], screenPts[0][1]); + for (let i = 1; i < screenPts.length; i++) { + ctx.lineTo(screenPts[i][0], screenPts[i][1]); + } + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + }; + + /** + * Draw a highlight overlay for a pick result returned by pickGeometryAsync. + * @param {any} result + * @return {Promise} + */ + setGeometryHighlightAsync = async (result) => { + this.#highlightResult = result; + await this.#drawHighlight(); + }; + + /** + * Clear the geometry highlight overlay. + */ + clearGeometryHighlight = () => { + this.#highlightResult = null; + const canvas = this.#highlightCanvas; + if (canvas) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + }; + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * @param {(fps:number)=>void} handler + * @return {()=>void} + */ + addFrameRenderedListener = (handler) => { + const remover = () => this.#frameRenderedListeners.delete(remover); + this.#frameRenderedListeners.set(remover, handler); + return remover; + }; + /** + */ + #setupPicker = () => { + const { renderer, interactor, picker, canvasDiv } = this; + const viewerClickedListeners = this.#viewerClickedListeners; + /////////////////////////////////////////// + /////////////////////////////////////////// + let pickingPending = false; + /**@type{number|null}*/ + let lastMouseX = null; + /**@type{number|null}*/ + let lastMouseY = null; + /////////////////////////////////////////// + /////////////////////////////////////////// + canvasDiv.addEventListener('blur', (e) => (lastMouseX = lastMouseY = null)); + canvasDiv.addEventListener('mouseout', (e) => (lastMouseX = lastMouseY = null)); + canvasDiv.addEventListener('mousedown', (e) => { + lastMouseX = e.clientX; + lastMouseY = e.clientY; + }); + canvasDiv.addEventListener('mouseup', async (e) => { + try { + if (lastMouseX == null || lastMouseY == null || pickingPending) { + return; + } else { + const margin = 0; + if (e.clientX < lastMouseX - margin || e.clientX > lastMouseX + margin) { + return; + } else if (e.clientY < lastMouseY - margin || e.clientY > lastMouseY + margin) { + return; + } + } + pickingPending = true; + const rect = canvasDiv.getBoundingClientRect(); + const normX = (e.clientX - rect.left) / rect.width; + const normY_vtk = 1.0 - (e.clientY - rect.top) / rect.height; + const [x, y] = await interactor.GetEventPosition(); + this.#lastPickDisplayPos = [x, y]; + const found = await picker.Pick([x, y, 0], renderer); + const actor = found ? await picker.getActor() : null; + for (const callback of viewerClickedListeners.values()) { + callback(actor?.id ?? null, e.ctrlKey, e.shiftKey, normX, normY_vtk); + } + } finally { + lastMouseX = lastMouseY = null; + pickingPending = false; + } + }); + }; + + /** + */ + #setupCamera = () => { + const { renderer, renderWindow, camera, canvasDiv, canvas } = this; + const cameraChangedListeners = this.#cameraChangedListeners; + + camera.observe('ModifiedEvent', () => { + for (const callback of cameraChangedListeners.values()) { + callback(camera); + } + }); + + /**@type{boolean}*/ + let eventFiring = false; + /**@type{boolean}*/ + let mouseLeftDown = false; + /**@type{boolean}*/ + let mouseMiddleDown = false; + /**@type{boolean}*/ + let mouseRightDown = false; + + /** + * @param {MouseEvent} e + * @param {number} buttonNumber + * @return {MouseEventInit} + */ + function getEventDict(e, buttonNumber) { + return { + clientX: e.clientX, + clientY: e.clientY, + button: buttonNumber, + bubbles: false, + cancelable: true, + }; + } + + /** + * @param {MouseEvent} e + * @param {boolean} down + */ + function applyMouseEvent(e, down) { + if (eventFiring) { + return; + } + try { + eventFiring = true; + if (e.button === 0) { + mouseLeftDown = down; + } else if (e.button === 1) { + mouseMiddleDown = down; + } else if (e.button === 2) { + mouseRightDown = down; + } + canvas.dispatchEvent(new MouseEvent('mouseup', getEventDict(e, 0))); + canvas.dispatchEvent(new MouseEvent('mouseup', getEventDict(e, 1))); + canvas.dispatchEvent(new MouseEvent('mouseup', getEventDict(e, 2))); + if (mouseLeftDown && mouseRightDown) { + canvas.dispatchEvent(new MouseEvent('mousedown', getEventDict(e, 1))); + e.stopPropagation(); + } else if (mouseRightDown) { + canvas.dispatchEvent(new MouseEvent('mousedown', getEventDict(e, 2))); + e.stopPropagation(); + } else if (mouseLeftDown) { + canvas.dispatchEvent(new MouseEvent('mousedown', getEventDict(e, 0))); + // don't call e.stopPropagation() here because we need to + // send the left-mouse-button event to the VTK canvas. + } else if (mouseMiddleDown) { + canvas.dispatchEvent(new MouseEvent('mousedown', getEventDict(e, 0))); + e.stopPropagation(); + } + } finally { + eventFiring = false; + } + } + + canvasDiv.addEventListener( + 'mousedown', + async (e) => { + applyMouseEvent(e, true); + }, + true + ); + canvasDiv.addEventListener( + 'contextmenu', + async (e) => { + // disable context menu + e.preventDefault(); + e.stopImmediatePropagation(); + }, + true + ); + canvasDiv.addEventListener( + 'mouseup', + async (e) => { + applyMouseEvent(e, false); + }, + true + ); + + // TODO: need to remove this event listener when the user disposes the WasmView object + window.addEventListener('keyup', async (e) => { + switch (e.key.toLowerCase()) { + case 'z': + await renderer.ResetCamera(); + await renderWindow.Render(); + return; + case 'r': + await camera.SetPosition(0, 0, 1); + await camera.SetFocalPoint(0, 0, 0); + await camera.SetViewUp(0, 1, 0); + await renderer.ResetCameraClippingPlane(); + await renderer.ResetCamera(); + await renderWindow.Render(); + return; + } + }); + canvasDiv.addEventListener( + 'mouseout', + async (e) => { + // This resolves the "sticky mousedown" issue. + // "Sticky mousedown" arises when the user holds left-click to rotate the + // mesh, but then moves the cursor out of the browser viewport while rotating, + // and releases the mouse button outside the viewport. When the user moves the + // cursor back into the viewport, the mesh continues to be rotated, even though + // the user is no longer holding down the mouse button. It's "sticky". + applyMouseEvent(e, false); + }, + true + ); + }; + + #setupFpsMonitor = () => { + const self = this; + const arr = /**@type{number[]}*/ []; + requestAnimationFrame(frame); + + function frame() { + const now = performance.now(); + arr.unshift(now); + if (arr.length >= 500) { + throw new Error(`array is too big: ${arr.length}`); + } + const nowMinusOneSecond = now - 1000; + for (let i = arr.length - 1; i >= 0; i--) { + if (arr[i] >= nowMinusOneSecond) { + arr.length = i + 1; + let dt = 0; + if (i > 0) { + dt = (arr[0] - arr[i]) / i; + } + const fps = 1000 / dt; + for (const callback of self.#frameRenderedListeners.values()) { + callback(fps); + } + requestAnimationFrame(frame); + return; + } + } + throw new Error('array should not be empty'); + } + }; +} diff --git a/src/ansys/visor/visor-client/src/wasm/VtkWasmPerfCounters.js b/src/ansys/visor/visor-client/src/wasm/VtkWasmPerfCounters.js new file mode 100644 index 00000000..81bf7795 --- /dev/null +++ b/src/ansys/visor/visor-client/src/wasm/VtkWasmPerfCounters.js @@ -0,0 +1,112 @@ +/** + * VtkWasmPerfCounters.js + * ------------------- + * Perf instrumentation for RemoteVtkScene's network fetch cycle. + * + * Used only when perf_logging is enabled (set via the server-side + * .visor config, propagated to the client through trame state). + * + * All log lines use the [PERF] prefix to match the server-side format, + * so they stand out consistently in the browser console. + */ + +const SLOW_BLOB_MS = 100; + +export class VtkWasmPerfCounter { + constructor() { + this.reset(); + } + + /** @type {number} */ + stateCount = 0; + /** @type {number} */ + stateTotalBytes = 0; + /** @type {number} */ + blobCount = 0; + /** @type {number} */ + blobTotalBytes = 0; + /** @type {number} */ + blobMaxBytes = 0; + /** @type {number} */ + blobMaxDur = 0; + + /** + * Reset all counters to zero in-place. + * Called by updateAsync at the start of each wasm update cycle. + */ + reset() { + this.stateCount = 0; + this.stateTotalBytes = 0; + this.blobCount = 0; + this.blobTotalBytes = 0; + this.blobMaxBytes = 0; + this.blobMaxDur = 0; + } + + /** + * Accumulate one state fetch result. + * @param {any} res + */ + recordStateFetch(res) { + this.stateCount++; + this.stateTotalBytes += res?.byteLength ?? res?.length ?? 0; + } + + /** + * Accumulate one blob fetch result into the counters. + * Also logs individually if the transfer was slow (โ‰ฅ SLOW_BLOB_MS), so a + * specific anomalous blob can be identified by hash โ€” the aggregate summary + * from logUpdateAsyncPerf already covers total/max across all blobs. + * @param {string} hash + * @param {any} res + * @param {number} dur elapsed ms + */ + recordBlobFetch(hash, res, dur) { + const size = res?.byteLength ?? res?.length ?? 0; + this.blobCount++; + this.blobTotalBytes += size; + if (size > this.blobMaxBytes) this.blobMaxBytes = size; + if (dur > this.blobMaxDur) this.blobMaxDur = dur; + if (dur >= SLOW_BLOB_MS) { + console.log(`[PERF] netFetchBlob hash=${hash} size=${size} dur=${dur.toFixed(2)}ms`); + } + } + + /** + * Log the per-status-fetch timing. + * @param {number} vtkId + * @param {number} dur elapsed ms + */ + logStatusFetch(vtkId, dur) { + console.log(`[PERF] netFetchStatus vtkId=${vtkId} dur=${dur.toFixed(2)}ms`); + } + + /** + * Emit the single [PERF] summary line for one updateAsync cycle and return + * the counters snapshot so the caller can forward it to the server. + * @param {number} wasmMs elapsed ms for the wasm update step + * @param {number} resizeMs elapsed ms for the resize step + * @return {{ stateCount:number, stateTotalBytes:number, blobCount:number, blobTotalBytes:number, blobMaxBytes:number, blobMaxDur:number, wasmMs:number, resizeMs:number, totalMs:number }} + */ + logUpdateAsyncPerf(wasmMs, resizeMs) { + const totalMs = wasmMs + resizeMs; + console.log( + `[PERF] updateAsync` + + ` | states=${this.stateCount} (${(this.stateTotalBytes / 1e6).toFixed(3)} MB)` + + ` | blobs=${this.blobCount} (${(this.blobTotalBytes / 1e6).toFixed(3)} MB total` + + `, largest=${(this.blobMaxBytes / 1e6).toFixed(3)} MB in ${this.blobMaxDur.toFixed(2)}ms)` + + ` | wasm=${wasmMs.toFixed(2)}ms resize=${resizeMs.toFixed(2)}ms total=${totalMs.toFixed(2)}ms` + ); + return { + stateCount: this.stateCount, + stateTotalBytes: this.stateTotalBytes, + blobCount: this.blobCount, + blobTotalBytes: this.blobTotalBytes, + blobMaxBytes: this.blobMaxBytes, + blobMaxDur: this.blobMaxDur, + wasmMs, + resizeMs, + totalMs, + }; + } +} diff --git a/src/ansys/visor/visor-client/src/widgets/boundingBoxWidget.ts b/src/ansys/visor/visor-client/src/widgets/boundingBoxWidget.ts new file mode 100644 index 00000000..06f8928a --- /dev/null +++ b/src/ansys/visor/visor-client/src/widgets/boundingBoxWidget.ts @@ -0,0 +1,85 @@ +import VtkScene from '../wasm/VtkScene'; + +function toNumericId(id: string | number | undefined, name = 'id'): number { + if (id === undefined || id === null) { + throw new Error(`Missing ${name}`); + } + const n = typeof id === 'number' ? id : Number(id); + if (!Number.isFinite(n)) { + throw new Error(`Invalid ${name}: ${id}`); + } + return n; +} + +export class BoundingBoxWidget { + constructor( + vtkScene: VtkScene, + sceneGraph: any, + ids: { + outlineActorId: string | number; + axesActorId: string | number; + boxAlgorithmWasmId: string | number; + outlineQueryId?: string | number; + } + ) { + const outlineQueryIdRaw = ids.outlineQueryId ?? ids.outlineActorId; + + // convert once and validate + const outlineQueryId = toNumericId(outlineQueryIdRaw, 'outlineQueryId'); + const outlineActorId = toNumericId(ids.outlineActorId, 'outlineActorId'); + const axesActorId = toNumericId(ids.axesActorId, 'axesActorId'); + const boxAlgorithmWasmId = toNumericId(ids.boxAlgorithmWasmId, 'boxAlgorithmWasmId'); + const outlineQuery = vtkScene!.getVtkObject(outlineQueryId); + const outlineActor = vtkScene!.getVtkObject(outlineActorId); + const axesActor = vtkScene!.getVtkObject(axesActorId); + const boxAlgorithm = vtkScene!.getVtkObject(boxAlgorithmWasmId); + const self = this; + + async function setVisibilityAsync(visible?: boolean) { + if (visible == null) { + // query current visibility (legacy VTK api) + const invokeResult = await outlineQuery.GetVisibility(); + const currentlyVisible = invokeResult === 1 || invokeResult === true; + self.#enabled = !currentlyVisible; + } else { + self.#enabled = visible; + } + await outlineActor.SetVisibility(self.#enabled ? 1 : 0); + await axesActor.SetVisibility(self.#enabled ? 1 : 0); + } + + async function updateBoundsAsync() { + let x_min = Number.MAX_SAFE_INTEGER; + let x_max = Number.MIN_SAFE_INTEGER; + let y_min = Number.MAX_SAFE_INTEGER; + let y_max = Number.MIN_SAFE_INTEGER; + let z_min = Number.MAX_SAFE_INTEGER; + let z_max = Number.MIN_SAFE_INTEGER; + + const nodes = sceneGraph?.descendantActorNodesOrSelfArray ?? []; + nodes.forEach((node: any) => { + if (!node.visible) return; + x_min = Math.min(x_min, node.bounds[0]); + x_max = Math.max(x_max, node.bounds[1]); + y_min = Math.min(y_min, node.bounds[2]); + y_max = Math.max(y_max, node.bounds[3]); + z_min = Math.min(z_min, node.bounds[4]); + z_max = Math.max(z_max, node.bounds[5]); + }); + + const finalBounds = [x_min, x_max, y_min, y_max, z_min, z_max]; + await boxAlgorithm.SetBounds([finalBounds]); + } + + this.setVisibilityAsync = setVisibilityAsync; + this.updateBoundsAsync = updateBoundsAsync; + } + + #enabled: boolean = false; + get enabled() { + return this.#enabled; + } + + setVisibilityAsync: (visible?: boolean) => Promise; + updateBoundsAsync: () => Promise; +} diff --git a/src/ansys/visor/visor-client/src/widgets/crossSectionWidget.ts b/src/ansys/visor/visor-client/src/widgets/crossSectionWidget.ts new file mode 100644 index 00000000..5774aacd --- /dev/null +++ b/src/ansys/visor/visor-client/src/widgets/crossSectionWidget.ts @@ -0,0 +1,88 @@ +import VtkScene from '../wasm/VtkScene'; + +function toNumericId(id: string | number | undefined, name = 'id'): number { + if (id === undefined || id === null) { + throw new Error(`Missing ${name}`); + } + const n = typeof id === 'number' ? id : Number(id); + if (!Number.isFinite(n)) { + throw new Error(`Invalid ${name}: ${id}`); + } + return n; +} + +/** + * createCrossSectionWidget encapsulates cross-section widget operations: + * - toggling visibility (queries widget for authoritative state when toggling) + * - checking enabled state + * - an interaction handler that copies origin/normal from the widget representation to the plane and triggers a render + * - updateBoundsAsync is provided as a placeholder (keeps parity with bounding-box API) + */ +export class CrossSectionWidget { + constructor( + vtkScene: VtkScene, + ids: { + planeWasmId: string | number; + representationWasmId: string | number; + widgetWasmId: string | number; + } + ) { + const planeId = toNumericId(ids.planeWasmId, 'planeWasmId'); + const repId = toNumericId(ids.representationWasmId, 'representationWasmId'); + const widgetId = toNumericId(ids.widgetWasmId, 'widgetWasmId'); + const widget = vtkScene.getVtkObject(widgetId); + const plane = vtkScene.getVtkObject(planeId); + const rep = vtkScene.getVtkObject(repId); + this.#rep = rep; + this.#plane = plane; + this.setVisibilityAsync = async (visible) => { + this.#enabled = visible ?? !this.#enabled; + if (this.#enabled) { + await widget.On(); + } else { + await widget.Off(); + } + }; + this.updateBoundsAsync = async () => { + // placeholder: cross-section bounds logic is currently empty in the original code + // keep a no-op to preserve API parity; implement later if needed + return; + }; + this.interactionHandler = async (/* event */) => { + // copy origin & normal from representation to the underlying plane and render + try { + const origin = await rep.GetOrigin(); + const normal = await rep.GetNormal(); + await plane.SetOrigin(origin); + await plane.SetNormal(normal); + vtkScene.render(); + } catch (e) { + // keep failures non-fatal (mirrors previous behavior) + console.debug('[CrossSectionWidget] interaction handler failed', e); + } + }; + } + + #rep: any; + #plane: any; + #enabled: boolean = false; + get enabled() { + return this.#enabled; + } + + getOriginAsync: () => Promise = async () => { + return await this.#rep.GetOrigin(); + }; + getNormalAsync: () => Promise = async () => { + return await this.#rep.GetNormal(); + }; + setOriginAsync: (origin: number[]) => Promise = async (origin) => { + await this.#plane.SetOrigin(origin); + }; + setNormalAsync: (normal: number[]) => Promise = async (normal) => { + await this.#plane.SetNormal(normal); + }; + setVisibilityAsync: (visible?: boolean) => Promise; + updateBoundsAsync: () => Promise; + interactionHandler: (event?: any) => Promise; +} diff --git a/src/ansys/visor/visor-client/src/widgets/edgesWidget.ts b/src/ansys/visor/visor-client/src/widgets/edgesWidget.ts new file mode 100644 index 00000000..f445a09a --- /dev/null +++ b/src/ansys/visor/visor-client/src/widgets/edgesWidget.ts @@ -0,0 +1,19 @@ +import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; + +export class EdgesWidget { + constructor(sceneGraph: VisorSceneNodeExtended) { + this.#sceneGraph = sceneGraph; + } + + #enabled: boolean = false; + get enabled() { + return this.#enabled; + } + + #sceneGraph: VisorSceneNodeExtended; + + setEdgesVisibleAsync = async (enable?: boolean | null): Promise => { + this.#enabled = enable ?? !this.#enabled; + await this.#sceneGraph.setEdgeVisibilityAsync(this.#enabled); + }; +} diff --git a/src/ansys/visor/visor-client/src/widgets/fullscreenWidget.ts b/src/ansys/visor/visor-client/src/widgets/fullscreenWidget.ts new file mode 100644 index 00000000..daff9fd3 --- /dev/null +++ b/src/ansys/visor/visor-client/src/widgets/fullscreenWidget.ts @@ -0,0 +1,21 @@ +export class FullscreenWidget { + constructor() {} + + isFullScreenAsync = async (): Promise => { + return document.fullscreenElement != null; + }; + setFullScreenAsync = async (enable?: boolean): Promise => { + if (enable == null) { + enable = !(await this.isFullScreenAsync()); + } + if (enable) { + if (document.fullscreenElement == null && document.fullscreenEnabled) { + await document.body.requestFullscreen(); + } + } else { + if (document.fullscreenElement != null) { + await document.exitFullscreen(); + } + } + }; +} diff --git a/src/ansys/visor/visor-client/src/widgets/orientationWidget.ts b/src/ansys/visor/visor-client/src/widgets/orientationWidget.ts new file mode 100644 index 00000000..34dbdc04 --- /dev/null +++ b/src/ansys/visor/visor-client/src/widgets/orientationWidget.ts @@ -0,0 +1,21 @@ +import VtkScene from '../wasm/VtkScene'; +import { type FrontendVtkObject } from '../wasm/RemoteVtkScene'; + +export class OrientationWidget { + constructor(vtkScene: VtkScene, orientationWidgetWasmId: number) { + this.#vtkScene = vtkScene; + this.widget = vtkScene.getVtkObject(orientationWidgetWasmId); + } + + widget: FrontendVtkObject; + #rep: FrontendVtkObject; + #vtkScene: VtkScene; + resizeAsync = async (widgetSize?: number) => { + this.#rep ??= await this.widget.GetRepresentation(); + if (widgetSize == null) { + const windowSize = await this.#vtkScene.renderWindow.GetSize(); + widgetSize = Math.round(Math.min(windowSize[0], windowSize[1]) * 0.13); + } + await this.#rep.SetSize([widgetSize, widgetSize]); + }; +} diff --git a/src/ansys/visor/visor-client/src/widgets/orthographicWidget.ts b/src/ansys/visor/visor-client/src/widgets/orthographicWidget.ts new file mode 100644 index 00000000..e7efcd4e --- /dev/null +++ b/src/ansys/visor/visor-client/src/widgets/orthographicWidget.ts @@ -0,0 +1,30 @@ +import VtkScene from '../wasm/VtkScene'; + +export class OrthographicWidget { + constructor(vtkScene: VtkScene) { + this.#vtkScene = vtkScene; + } + + #enabled: boolean = false; + get enabled() { + return this.#enabled; + } + + #vtkScene: VtkScene; + + setOrthographicModeAsync = async (enable?: boolean | null): Promise => { + if (enable == null) { + enable = !(await this.isOrthographicAsync()); + } + if ((this.#enabled = enable)) { + await this.#vtkScene.camera.ParallelProjectionOn(); + } else { + await this.#vtkScene.camera.ParallelProjectionOff(); + } + }; + + isOrthographicAsync = async (): Promise => { + const result = await this.#vtkScene.camera.GetParallelProjection(); + return result === 1 || result === true; + }; +} diff --git a/src/ansys/visor/visor-client/tsconfig.app.json b/src/ansys/visor/visor-client/tsconfig.app.json new file mode 100644 index 00000000..797dfc82 --- /dev/null +++ b/src/ansys/visor/visor-client/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "types": ["jest"] + }, + "include": ["src"] +} diff --git a/src/ansys/visor/visor-client/tsconfig.json b/src/ansys/visor/visor-client/tsconfig.json new file mode 100644 index 00000000..41ccf0f7 --- /dev/null +++ b/src/ansys/visor/visor-client/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/src/ansys/visor/visor-client/tsconfig.node.json b/src/ansys/visor/visor-client/tsconfig.node.json new file mode 100644 index 00000000..55e32ced --- /dev/null +++ b/src/ansys/visor/visor-client/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "types": ["jest"] + }, + "include": ["vite.config.ts"] +} diff --git a/src/ansys/visor/visor-client/vite.config.ts b/src/ansys/visor/visor-client/vite.config.ts new file mode 100644 index 00000000..528c2060 --- /dev/null +++ b/src/ansys/visor/visor-client/vite.config.ts @@ -0,0 +1,221 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { copyFile, mkdir } from 'fs/promises'; +import fs from 'fs'; +import path from 'path'; +import * as zlib from 'zlib'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + { + name: 'modify-text-file', + async closeBundle() { + // copy client bundle files from /visor-client/modules/wasm to /viewer/client_bundle/wasm + const outDir = this.environment.config.build.outDir; + const vtkAssetDir = path.resolve(outDir, 'wasm'); + await mkdir(vtkAssetDir, { recursive: true }); + { + await copyAsset('vtkWebAssembly.wasm'); + await copyAsset('vtkWebAssembly.mjs'); + await copyAsset('vtkWebAssemblyAsync.wasm'); + await copyAsset('vtkWebAssemblyAsync.mjs'); + + async function copyAsset(fileName: string) { + const src = path.resolve(__dirname, `modules/wasm/${fileName}`); + const dest = path.resolve(vtkAssetDir, fileName); + await copyFile(src, dest); + } + } + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + let reactJsPath: string | null = null; + let reactCssPath: string | null = null; + const reactAssetDir = path.resolve(outDir, 'assets'); + const reactAssetPaths = await fs.promises.readdir(reactAssetDir); + if (reactAssetPaths.length != 2) { + const msg = `There should be exactly 2 files in the React asset directory (js and`; + throw new Error(`${msg} css). Directory searched: '${reactAssetPaths}'`); + } + reactAssetPaths.forEach((filename) => { + if (/\.js$/i.test(filename)) { + reactJsPath = path.join(reactAssetDir, filename); + } else if (/\.css$/i.test(filename)) { + reactCssPath = path.join(reactAssetDir, filename); + } + }); + if (reactJsPath == null) { + const msg = `No JS file was found in React asset`; + throw new Error(`${msg} directory '${reactAssetDir}'`); + } + if (reactCssPath == null) { + const msg = `No CSS file was found in React asset`; + throw new Error(`${msg} directory '${reactAssetDir}'`); + } + console.log(`React JS path: '${reactJsPath}'`); + console.log(`React CSS path: '${reactCssPath}'`); + // Rewrite legacy names before reading JS/CSS so generated Python helpers + // will contain the rewritten content. + try { + rewriteLegacyNames(outDir); + } catch (e) { + console.warn('rewriteLegacyNames failed:', e); + } + const reactJs = await fs.promises.readFile(reactJsPath, 'utf-8'); + const reactCss = await fs.promises.readFile(reactCssPath, 'utf-8'); + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + const vtkMjsPath = path.join(vtkAssetDir, 'vtkWebAssembly.mjs'); + const vtkWasmPath = path.join(vtkAssetDir, 'vtkWebAssembly.wasm'); + const vtkAsyncMjsPath = path.join(vtkAssetDir, 'vtkWebAssemblyAsync.mjs'); + const vtkAsyncWasmPath = path.join(vtkAssetDir, 'vtkWebAssemblyAsync.wasm'); + console.log(`VTK MJS path: '${vtkMjsPath}'`); + console.log(`VTK WASM path: '${vtkWasmPath}'`); + console.log(`VTK ASYNC MJS path: '${vtkAsyncMjsPath}'`); + console.log(`VTK ASYNC WASM path: '${vtkAsyncWasmPath}'`); + const vtkWasmZipped = zlib.gzipSync(await fs.promises.readFile(vtkWasmPath), { + level: zlib.constants.Z_BEST_COMPRESSION, + }); + const vtkAsyncWasmZipped = zlib.gzipSync( + await fs.promises.readFile(vtkAsyncWasmPath), + { + level: zlib.constants.Z_BEST_COMPRESSION, + } + ); + const vtkMjs = await fs.promises.readFile(vtkMjsPath, 'utf-8'); + const vtkWasmBase64 = vtkWasmZipped.toString('base64'); + const vtkAsyncMjs = await fs.promises.readFile(vtkAsyncMjsPath, 'utf-8'); + const vtkAsyncWasmBase64 = vtkAsyncWasmZipped.toString('base64'); + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + const cssDir = path.resolve(outDir, 'css'); + const darkThemePath = path.join(cssDir, 'theme-dark.css'); + console.log(`Dark theme path: '${darkThemePath}'`); + const lightThemePath = path.join(cssDir, 'theme-light.css'); + console.log(`Light theme path: '${lightThemePath}'`); + const darkThemeCss = await fs.promises.readFile(darkThemePath, 'utf-8'); + const lightThemeCss = await fs.promises.readFile(lightThemePath, 'utf-8'); + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + const fontDir = path.resolve(outDir, 'fonts'); + const fontPath = path.join(fontDir, 'source-sans-3.woff2'); + console.log(`Font path: '${fontPath}'`); + const fontZipped = zlib.gzipSync(await fs.promises.readFile(fontPath)); + const fontBase64 = fontZipped.toString('base64'); + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + + function stringToFunction(funcName: string, str: string) { + let python = `def ${funcName}():\n`; + python += `\tparts: [str] = [\n`; + for (const match of str.matchAll(/.{1,100}/gs)) { + python += `\t\t"${escape(match[0])}",\n`; + } + python += '\t]\n'; + python += '\treturn "".join(parts)\n'; + return python; + + function escape(str: string) { + str = str.replace(/\\/g, '\\\\'); + str = str.replace(/"/g, '\\"'); + str = str.replace(/'/g, "\\'"); + str = str.replace(/\n/g, '\\n'); + str = str.replace(/\r/g, '\\r'); + str = str.replace(/\t/g, '\\t'); + str = str.replace(/\f/g, '\\f'); + // eslint-disable-next-line no-control-regex + str = str.replace(/\x08/g, '\\b'); + return str; + } + } + + let reactAssetsPython = stringToFunction('getJs', reactJs); + reactAssetsPython += stringToFunction('getCss', reactCss); + let vtkAssetsPython = stringToFunction('getVtkMjs', vtkMjs); + vtkAssetsPython += stringToFunction('getVtkWasmBase64', vtkWasmBase64); + let vtkAsyncAssetsPython = stringToFunction('getVtkAsyncMjs', vtkAsyncMjs); + vtkAsyncAssetsPython += stringToFunction( + 'getVtkAsyncWasmBase64', + vtkAsyncWasmBase64 + ); + let cssPython = stringToFunction('getDarkThemeCss', darkThemeCss); + cssPython += stringToFunction('getLightThemeCss', lightThemeCss); + const fontPython = stringToFunction('getFontBase64', fontBase64); + /////////////////////////////////////////////////////////////////////////////////////// + const dashProjectDir = path.resolve(outDir, '..', '..'); + console.log(`Dash project directory: '${dashProjectDir}'`); + const dashPackageDir = path.join(dashProjectDir, 'dash', 'visordash'); + console.log(`Dash package directory: '${dashPackageDir}'`); + await fs.promises.mkdir(dashPackageDir, { recursive: true }); + /////////////////////////////////////////////////////////////////////////////////////// + const reactAssetsPythonPath = path.join( + dashPackageDir, + '__GITIGNORE_visor_react_assets.py' + ); + await fs.promises.writeFile(reactAssetsPythonPath, reactAssetsPython, 'utf-8'); + console.log(`Created file: '${reactAssetsPythonPath}'`); + /////////////////////////////////////////////////////////////////////////////////////// + const vtkAssetsPythonPath = path.join( + dashPackageDir, + '__GITIGNORE_visor_vtk_assets.py' + ); + await fs.promises.writeFile(vtkAssetsPythonPath, vtkAssetsPython, 'utf-8'); + console.log(`Created file: '${vtkAssetsPythonPath}'`); + /////////////////////////////////////////////////////////////////////////////////////// + const vtkAsyncAssetsPythonPath = path.join( + dashPackageDir, + '__GITIGNORE_visor_vtk_async_assets.py' + ); + await fs.promises.writeFile( + vtkAsyncAssetsPythonPath, + vtkAsyncAssetsPython, + 'utf-8' + ); + console.log(`Created file: '${vtkAsyncAssetsPythonPath}'`); + /////////////////////////////////////////////////////////////////////////////////////// + const cssPythonPath = path.join(dashPackageDir, '__GITIGNORE_visor_css.py'); + await fs.promises.writeFile(cssPythonPath, cssPython, 'utf-8'); + console.log(`Created file: '${cssPythonPath}'`); + /////////////////////////////////////////////////////////////////////////////////////// + const fontPythonPath = path.join(dashPackageDir, '__GITIGNORE_visor_font.py'); + await fs.promises.writeFile(fontPythonPath, fontPython, 'utf-8'); + console.log(`Created file: '${fontPythonPath}'`); + }, + }, + ], + build: { + minify: false, + outDir: '../viewer/client_bundle', + emptyOutDir: true, + }, +}); + +// Post-build: ensure any legacy vtkWasmSceneManager identifiers emitted +// inside bundled assets are rewritten to vtkWebAssembly to avoid runtime +// fallback probing. This is a lightweight deterministic text-replacement +// that keeps the bundle clean without editing dependency sources. +function rewriteLegacyNames(outDir: string) { + const assetsDir = path.resolve(outDir, 'assets'); + if (!fs.existsSync(assetsDir)) return; + for (const file of fs.readdirSync(assetsDir)) { + const full = path.join(assetsDir, file); + if (!/\.js$/.test(file)) continue; + let txt = fs.readFileSync(full, 'utf-8'); + const before = txt; + // Replace the legacy bundle name with the modern one + txt = txt.replace(/vtkWasmSceneManager/g, 'vtkWebAssembly'); + if (txt !== before) { + fs.writeFileSync(full, txt, 'utf-8'); + console.log(`Rewrote legacy wasm name in ${full}`); + } + } +} + +// Export helper for manual invocation after build if needed. +export { rewriteLegacyNames }; diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/start_visor.http b/tests/api/start_visor.http new file mode 100644 index 00000000..5d485ff1 --- /dev/null +++ b/tests/api/start_visor.http @@ -0,0 +1,11 @@ +POST http://localhost:53211/start +Content-Type: application/json + +{ + "file_path": "tests/files/many_blocks/many_blocks.vtm", + "metadata": { + "name": "Many Blocks Asset", + "unit": "m" + }, + "timeout": 60 +} diff --git a/tests/api/stop_server.http b/tests/api/stop_server.http new file mode 100644 index 00000000..c3292d22 --- /dev/null +++ b/tests/api/stop_server.http @@ -0,0 +1,2 @@ +POST http://localhost:53211/stop +Content-Type: application/json diff --git a/tests/api/update_server.http b/tests/api/update_server.http new file mode 100644 index 00000000..2592c14f --- /dev/null +++ b/tests/api/update_server.http @@ -0,0 +1,10 @@ +POST http://localhost:53211/update +Content-Type: application/json + +{ + "file_path": "tests/files/plate.vtp", + "metadata": { + "name": "Plate Asset", + "unit": "cm" + } +} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..0abb642f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited. + +# ==================================================== [Imports] ==================================================== # + +from pathlib import Path + +import pytest + +# =================================================== [Variables] =================================================== # + + +# =================================================== [Functions] =================================================== # + +# Add options for pytest-image comparison common operations +def pytest_addoption(parser: pytest.Parser): + parser.addoption("--update-baseline", action="store_true", default=False, help="Overwrite baseline image with current screenshot.") + parser.addoption("--baseline-dir", action="store", default="tests/references", help="Directory where baseline images are stored.") + parser.addoption("--pixel-threshold", action="store", default="2.55", help="RMS threshold for image comparison (0..255).") + # parser.addoption("--url", action="store", default="http://localhost:8081/index.html", help="Target URL for ST-03.") + parser.addoption("--dash-port", action="store", default="8050", help="Port where the Dash app will run.") + # parser.addoption("--visor-port", action="store", default="8081", help="Port where the Visor will run.") + parser.addoption("--host", action="store", default="127.0.0.1", help="Target host for servers.") + parser.addoption("--saf-project-dir", action="store", default=None, help="Path to saf-visor-poc root directory (overrides SAF_VISOR_POC_DIR env var).") + + +# Base fixtures ----------------------------------- + +@pytest.fixture(scope="module") +def baseline_dir(request) -> Path: + return Path(request.config.getoption("--baseline-dir")).resolve() + +@pytest.fixture(scope="module") +def pixel_threshold(request) -> float: + return float(request.config.getoption("--pixel-threshold")) + +@pytest.fixture +def suite_name(request): + # get all markers applied to this test + markers = request.node.iter_markers() + names = [m.name for m in markers] + # return only the first marker name + return names[0] \ No newline at end of file diff --git a/tests/docs/VisorPythonClassExamples.ipynb b/tests/docs/VisorPythonClassExamples.ipynb new file mode 100644 index 00000000..d96a3dbd --- /dev/null +++ b/tests/docs/VisorPythonClassExamples.ipynb @@ -0,0 +1,427 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "c64747ab-814a-4c0d-86d1-5d72b9565e88", + "metadata": {}, + "outputs": [], + "source": [ + "##########################################################################################\n", + "# Example smoke tests for Visor - using native Python class\n", + "#\n", + "# Intended as representative tests, not exhaustive\n", + "##########################################################################################" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4ad5db7d-7c81-4f84-bede-ea7e70592b76", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "18d15565-87c4-41a5-ae97-21f90cb7558b", + "metadata": {}, + "outputs": [], + "source": [ + "from ansys.visor.viewer import Visor" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3a811499-d6ec-495d-87b2-b82e01787c85", + "metadata": {}, + "outputs": [], + "source": [ + "DATASET1_PATH = \"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm\"\n", + "DATASET1_METADATA_PATH = \"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json\"\n", + "\n", + "DATASET2_PATH = \"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm\"\n", + "DATASET2_METADATA_PATH = \"../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8e986522-dae7-4fab-97eb-d607b25f31b7", + "metadata": {}, + "outputs": [], + "source": [ + "####################################################################\n", + "# Step 0: Instantiate a Visor class\n", + "####################################################################\n", + "visualizer = Visor() # spins up Visor class on URL http://localhost:8081" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b9eaf2d2-3a9b-4668-a127-f7b4852bb2de", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server starting...\n", + "Server started and ready on http://localhost:8081\n" + ] + }, + { + "data": { + "text/plain": [ + "2731713415694765" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "####################################################################\n", + "# Step 1: Start a Visor session with multiblock + metadata\n", + "####################################################################\n", + "\n", + "visualizer.start(input=DATASET1_PATH, metadata=DATASET1_METADATA_PATH)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c43d38bb-58df-4c07-adcf-39c8b1da4e7b", + "metadata": {}, + "outputs": [], + "source": [ + "# Check that the dataset loads correctly:\n", + "# * meshes look as expected\n", + "# * the opacities are correct in the slider\n", + "# * and opacities are applied correctly on the mesh.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4d3f3ce5-6700-418d-bd9e-a8a2de4fba93", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'app_name': 'Visor Viewer',\n", + " 'host': 'localhost',\n", + " 'port': 8081,\n", + " 'standalone': True,\n", + " 'datasets': ['vtk_scene_sphere_l2_b3_r32_v3_c1_z0'],\n", + " 'metadata': None}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "####################################################################\n", + "# Step 2: Print info about the instance\n", + "####################################################################\n", + "visualizer.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "348b9369-2f4a-4424-9280-e9b719986996", + "metadata": {}, + "outputs": [], + "source": [ + "####################################################################\n", + "# Step 3: Adjust the settings on parts\n", + "# \n", + "# Adjust settings on the following parts:\n", + "# * level_0_block_0_part_0_parent:\n", + "# * Adjust opacity to something high but not 100%, e.g. 95%\n", + "# * Color by constant (i.e. don't change the dropdown color by variable value)\n", + "# * Change the Constant Color to something different\n", + "# * level_0_block_0_part_1_parent:\n", + "# * Color by POINT -random_variable_2 (Scalar)\n", + "# * level_0_block_0_part_2_parent:\n", + "# * Color by POINT - gradient_variable_0 (Scalar)\n", + "# * level_1_block_1_part_1 (or any individual sphere):\n", + "# * Toggle visibility OFF\n", + "# * Check that all settings are applied correctly.\n", + "####################################################################\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8bff4b3b-23df-4035-a9f9-beabe4ba84e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2731261597931916" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "####################################################################\n", + "# Step 4: Add a new dataset to the scene\n", + "####################################################################\n", + "visualizer.add_dataset(input=DATASET2_PATH, metadata=DATASET2_METADATA_PATH)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b570e2a-715a-4de0-9ba1-0c6ccfd33fd2", + "metadata": {}, + "outputs": [], + "source": [ + "# Check that the new dataset loads correctly:\n", + "# * meshes look as expected\n", + "# * the opacities are correct in the slider\n", + "# * and opacities are applied correctly on the mesh.\n", + "# Check that the first dataset is still present, with part properties preserved from before the add_dataset operation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1e9620ee-15bc-436b-a873-f3b9767a3bb2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{2731713415694765: {'id': 2731713415694765,\n", + " 'name': 'vtk_scene_sphere_l2_b3_r32_v3_c1_z0',\n", + " 'unit': 'm',\n", + " 'file_path': '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm',\n", + " 'metadata_path': '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json'},\n", + " 2731261597931916: {'id': 2731261597931916,\n", + " 'name': 'vtk_scene_sphere_l2_b3_r32_v3_c1_z0_1',\n", + " 'unit': 'm',\n", + " 'file_path': '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm',\n", + " 'metadata_path': '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json'}}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "####################################################################\n", + "# Step 5. List datasets\n", + "####################################################################\n", + "visualizer.list_datasets()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bb30bc00-52f0-443e-b790-e1f0c4d825ca", + "metadata": {}, + "outputs": [], + "source": [ + "####################################################################\n", + "# Step 6. Remove the second dataset\n", + "####################################################################\n", + "visualizer.remove_dataset(2731261597931916)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3aa97cbb-79b8-494f-8642-1011587f24a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Check that\n", + "# * the dataset was removed in the viewer, and\n", + "# * the other dataset still has the expected per-part settings applied.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "85891b14-c112-456c-b546-769f35095289", + "metadata": {}, + "outputs": [], + "source": [ + "####################################################################\n", + "# Step 7. Save the state\n", + "####################################################################\n", + "await visualizer.save_state(\"test_dir\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "08d13fce-3624-4038-bf57-c28d75ea9016", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "visor.json\n", + "vtk_scene_sphere_l2_b3_r32_v3_c1_z0_snapshot.vtkhdf\n" + ] + } + ], + "source": [ + "# Check that the state visor.json file and dataset vtkhdf file were serialized to disk\n", + "for item in Path(\"test_dir\").iterdir():\n", + " print(item.name)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "74833969-934d-4e84-9bb9-a74b6f9aab53", + "metadata": {}, + "outputs": [], + "source": [ + "####################################################################\n", + "# Step 7. Load the state on already-loaded dataset\n", + "####################################################################\n", + "\n", + "# Before loading the state, change scene settings (eg toggle visibility,\n", + "# color-by, or expand/minimize some panels, or change the camera\n", + "visualizer.load_state(\"test_dir\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8e65659-1042-453b-8aff-cdb28f90d06d", + "metadata": {}, + "outputs": [], + "source": [ + "# Check that the state was loaded as expected" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "00eaf381-a6a3-4e91-b6d1-f0683fcdd3f6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{2731713415694765: {'id': 2731713415694765,\n", + " 'name': 'vtk_scene_sphere_l2_b3_r32_v3_c1_z0',\n", + " 'unit': 'm',\n", + " 'file_path': '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm',\n", + " 'metadata_path': '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json'}}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "####################################################################\n", + "# Step 8. Load the state on empty scene\n", + "####################################################################\n", + "# First remove datasets from scene\n", + "visualizer.list_datasets()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "bdfa7159-120e-457d-a9d8-90d25c823f15", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.remove_dataset(2731713415694765)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "c6fa059d-5643-46cc-936c-72aa88ecb141", + "metadata": {}, + "outputs": [], + "source": [ + "# Confirm the dataset was removed and scene is now empty" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "907489f7-08ee-432c-9390-4bbae00b2669", + "metadata": {}, + "outputs": [], + "source": [ + "visualizer.load_state(\"test_dir\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ed9763d-707c-415e-97fd-eccc7abe8154", + "metadata": {}, + "outputs": [], + "source": [ + "# Check that the dataset was loaded and saved state was applied." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38abbb79-86cd-4854-8774-e4004731f9c8", + "metadata": {}, + "outputs": [], + "source": [ + "####################################################################\n", + "# Note this notebook is not at all exhaustive; there are many permutations\n", + "# of the above that can also be tested, e.g.:\n", + "# * save/load state configurations (ie ensuring all elements are saved and loaded as intended)\n", + "# * various combinations of arguments for the methods above\n", + "####################################################################\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/docs/images/regression_test_1.png b/tests/docs/images/regression_test_1.png new file mode 100644 index 00000000..9f7604f5 Binary files /dev/null and b/tests/docs/images/regression_test_1.png differ diff --git a/tests/docs/images/regression_test_2.png b/tests/docs/images/regression_test_2.png new file mode 100644 index 00000000..1e859af1 Binary files /dev/null and b/tests/docs/images/regression_test_2.png differ diff --git a/tests/docs/images/regression_test_3.png b/tests/docs/images/regression_test_3.png new file mode 100644 index 00000000..b4f4cc56 Binary files /dev/null and b/tests/docs/images/regression_test_3.png differ diff --git a/tests/docs/images/regression_test_4.png b/tests/docs/images/regression_test_4.png new file mode 100644 index 00000000..b7b1ce00 Binary files /dev/null and b/tests/docs/images/regression_test_4.png differ diff --git a/tests/docs/images/regression_test_5.png b/tests/docs/images/regression_test_5.png new file mode 100644 index 00000000..b4f4cc56 Binary files /dev/null and b/tests/docs/images/regression_test_5.png differ diff --git a/tests/docs/regression_test_summary.md b/tests/docs/regression_test_summary.md new file mode 100644 index 00000000..437f535a --- /dev/null +++ b/tests/docs/regression_test_summary.md @@ -0,0 +1,403 @@ +# Regression Testing Coverage for Visor + +*April 2026* + +--- + +## Overview + +This document is a rough shared reference for the team, collecting representative tests that have been +useful when checking changes, so everyone has a common picture to work from and can contribute their own. + +It is not a formal test plan or test strategy, and is not meant to be exhaustive or authoritative. The goal is +to write down what we already do, so the knowledge is shared rather than scattered, and so the tests outlined +here can provide info for our broader automated testing strategy. + +Automated coverage for everything listed here is a work in progress, and this document also serves as a +running reference for what we want to grow towards as that coverage expands. + +## A. Supported Environments, APIs, and Viewer Elements + +### 1. Launch Environments + +Visor has two main supported launch environments: + +- **Service:** visor-cli +- **Native Python:** Jupyter notebook + +### 2. Display Environments + +There are three main supported test display environments: + +- Raw browser +- Dash component +- Iframe within Jupyter notebook + +### 3. APIs + +We need to test all the public APIs using both the service and native Python. + +1. `initialize` (`visor-cli init`) +2. `info` +3. `health` +4. `start` +5. `update` +6. `clear` +7. `add_dataset` +8. `remove_dataset` +9. `list_datasets` +10. `list_variables` +11. `update_variables`* +12. `save_state` +13. `load_state` +14. stop +15. stop_visualization +> \* `update_variables` is not exposed through visor-cli. It can be tested using the HTTP API directly, or via native Python. + +### 4. Input Formats + +The following APIs accept an input dataset, along with an optional metadata argument: + +- `start` +- `update` +- `add_dataset` + +There are several combinations of how we can pass the input dataset and optional metadata args, which is relevant for testing. + +#### HTTP vs Native Python Inputs + +The HTTP APIs accept a file path as the input dataset. The native Python APIs (i.e. the methods on the `Visor` class) accept either a file, or a VTK dataset object, as an input. + +| | **File** | **VtkDataSet object** | **File Metadata** | **Metadata object** | +|--|----------|-----------------------|-------------------|---------------------| +| **HTTP API** | Yes | No *(streaming not yet supported)* | Yes | Yes *(pass serialized as JSON, gets deserialized into `Metadata` class)* | +| **Native Python Methods** | Yes | Yes | Yes | Yes *(native Python `Metadata` object)* | + +#### Required vs Optional Input Args + +The `start` API does not require an input dataset, but the `update` and `add_dataset` APIs do. The `Metadata` class is optional for all. + +| API | Input required? | Metadata required? | +|-----|-----------------|--------------------| +| `start` | No | No | +| `update` | Yes | No | +| `add_dataset` | Yes | No | + +#### Input Dataset Formats + +The following file formats and VTK dataset types are accepted: + +| VTK Dataset Object | File extension when serialized | Composite? | +|--------------------|-------------------------------|------------| +| `vtkPolyData` | `.vtp`, `.vtkhdf` | No | +| `vtkUnstructuredGrid` | `.vtu`, `.vtkhdf` | No | +| `vtkMultiBlockDataSet` | `.vtm`, `.vtkhdf` | Yes | + +> **Note:** `vtkMultiPieceDataSet` datasets loaded from a `.vtm` file are in scope for Visor but are not currently supported. In the future, we will also need to support `.vtkhdf` loading assemblies and partitions (which supersedes the older multi piece dataset), but this is not yet supported in Visor. + +### 5. Viewer Elements + +List of elements in the viewer that should be checked at each step: + +- Tree view +- Top right window +- Legend +- Widgets at bottom + +### 6. Viewer Interactions + +List of actions to take in the viewer at each step to make sure all is working correctly: + +- Select part in tree view +- Select part using picker +- Toggle visibility using tree view +- Adjust opacity via slider or text input in Properties panel +- Change diffuse color using color picker in Properties panel +- Color part by variable using Legend panel variable selector +- Select variable component (Magnitude, X, Y, Z, etc.) in Legend panel +- Adjust legend range min/max and apply +- Toggle perspective/orthographic projection (toolbar) +- Toggle cross-section plane (toolbar) and adjust plane interactively +- Toggle edges/wireframe display (toolbar) +- Toggle fullscreen (toolbar) +- Toggle bounding box display (toolbar) +- Orbit/pan/zoom camera +- Search/filter nodes in tree view +- Collapse/expand side panels +- etc. + +--- + +## B. Regression Tests + +Here we list a comprehensive set of tests to run to ensure the behaviour in Visor remains as expected for basic usage. + +Run the following commands in a terminal window, using the `visor-cli` tool. Note that we need to run the comparable tests using a Jupyter notebook as well. + +### 1. Test Files + +The following files are used for testing below: + +| Type | Description | Unique part names? | File name | Metadata file | +|------------------|----------------------------------------------------|--------------------|-----------------------------------------------------------|---------------| +| MultiBlock | Multiblock of 6 spheres | Yes | `examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm` | `examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json` | +| Multiblock | Multiblock of 6 spheres, translated in z direction | Yes | `examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm` | `examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json` | +| Multiblock | Cylinder multiblock | No | `tests/files/many_blocks/many_blocks.vtm` | | +| PolyData | Rectangular plate | - | `tests/files/plate.vtp` | | +| UnstructuredGrid | Triangular prism | - | `tests/files/mesh.vtu` | | + + +### 2. HTTP API Tests + +The HTTP APIs can be tested in two main ways: + +1. **visor-cli command line tool:** A helper tool that can run the Visor service and interact with it by submitting requests. The primary limitation is that the `update_variables` API is not currently exposed via the CLI tool, so it needs to be tested directly with an HTTP request. +2. **HTTP requests:** Any of the APIs can be tested directly with HTTP requests (e.g. using the Python `requests` library). Currently the only API that needs to be tested via a direct HTTP request (vs the CLI tool) is the `update_variables` API. + + +#### A. Visor CLI Tool + +Before running the following commands, run: + +```bash +visor-cli server start +``` + + +##### Step 1. Start a Visor session with a multiblock file + metadata file + +```bash +visor-cli instance start examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm --metadata-path examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json +``` + +**Check that:** +- The dataset loads correctly: meshes look as expected, the opacities are correct in the slider, and opacities are applied correctly on the mesh. + + +##### Step 2. Print info about the service's active instance + +```bash +visor-cli instance info +``` + +**Check that** the API runs and the output is correct: + +```json +{'app_name': 'Visor Viewer', 'host': 'localhost', 'port': 8081, 'standalone': True, 'datasets': ['vtk_scene_sphere_l2_b3_r32_v3_c1_z0']} +``` + + +##### Step 3. Adjust settings on parts + +Adjust settings on the following parts: + +- **`level_0_block_0_part_0_parent`:** + - Adjust opacity to something high but not 100%, e.g. 95% + - Color by constant (i.e. don't change the dropdown color by variable value) + - Change the Constant Color to something different + +- **`level_0_block_0_part_1_parent`:** + - Color by POINT -`random_variable_2` (Scalar) + +- **`level_0_block_0_part_2_parent`:** + - Color by POINT - `gradient_variable_0` (Scalar) + +- **`level_1_block_1_part_1`** (or any individual sphere): + - Toggle visibility **OFF** + +**Check that** all settings are applied correctly. + +Viewer after adjusting settings on parts + +##### Step 4. Add a new dataset to the scene + +```bash +visor-cli instance add examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm --metadata-path examples\assets\vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json +``` + +**Check that:** +- The new dataset loads correctly: meshes look as expected, the opacities are correct in the slider, and opacities are applied correctly on the mesh. +- The first dataset is still present, with part properties preserved from before the `add_dataset` operation. + +Viewer after adding dataset + +##### Step 5. List datasets + +```bash +visor-cli instance list +``` + +**Confirm** the output shows correct information about both datasets: + +```json +{ + "6857852647438280": { + "id": 6857852647438280, + "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z0", + "unit": "m", + "file_path": "examples\\assets\\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm", + "metadata_path": "examples\\assets\\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json" + }, + "3610179548300432": { + "id": 3610179548300432, + "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z3", + "unit": "m", + "file_path": "examples\\assets\\vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm", + "metadata_path": "examples\\assets\\vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json" + } +} +``` + + +##### Step 6. Remove the second dataset + +```bash +visor-cli instance remove 3610179548300432 +``` + +**Check that** the dataset was removed in the viewer, and that the other dataset still has the expected per-part settings applied. + +Viewer after removing second dataset + +##### Step 7. Save the state + +```bash +visor-cli instance save saved_state +``` + +**Confirm** the state was saved to the specified directory: + +``` +(.venv) ?? ls saved_state +visor.json vtk_scene_sphere_l2_b3_r32_v3_c1_z0_snapshot.vtkhdf +``` + + +##### Step 8. Test `load_state` on already-loaded asset + +Refresh the browser and load the state: + +```bash +visor-cli instance load saved_state +``` + +**Check that** the state is reapplied correctly on the dataset. + + +##### Step 9. Test `load_state` on empty scene + +Remove the dataset from the scene: + +```bash +visor-cli instance list # get the dataset ID if needed +``` + +```json +{ + "6857852647438280": { + "id": 6857852647438280, + "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z0", + "unit": "m", + "file_path": "examples\\assets\\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm", + "metadata_path": "examples\\assets\\vtk_scene_sphere_l2_b3_r32_v3_c1_z0.json" + } +} +``` + +```bash +visor-cli instance remove 6857852647438280 +``` + +Viewer after removing all datasets + +**Confirm** there is an empty scene, then load the saved state: + +```bash +visor-cli instance load saved_state +``` + +**Check that** the dataset was loaded and the saved state was applied. + +Viewer after loading state from empty scene + +##### Step 10. Further Tests of Save/Load State + +There are many elements to the save/load state feature. Ideally we will have a separate section dedicated to this feature. + +> **TODO:** *Create separate list for save/load state regression tests, covering all supported save/load state elements* +##### Step 11. Stop visualization + +```bash +visor-cli instance stop_visualization +``` + +##### Step 12. Stop and remove the instance + +```bash +visor-cli instance stop +``` +#### B. HTTP Requests + +The `visor-cli` tool currently wraps all APIs except one (`update_variables`). If there is a need to test all the APIs directly using HTTP requests, the steps in the previous section should be replicated using direct HTTP requests. + +To test `update_variables` using a HTTP request, a Jupyter notebook is included in the Visor repository to demonstrate how to test via a HTTP request: + +``` +examples/notebooks/Variable Update Testing ??? Plate Example HTTP API.ipynb +``` + + +### 3. Native Python Tests + +The tests outlined in the HTTP API section above also need to be tested via the native Python class directly. + +These can be tested either in a Python script or in a Jupyter notebook. Jupyter notebooks are a primary supported +environment for Visor, but for automated testing, it can be difficult to set up and maintain a long list of tests in a +Jupyter notebook. + +We expect to have our primary regression tests for the native Python class in Python scripts that can be run +automatically as part of a CI pipeline, with a few targeted tests automated in a Jupyter notebook. (We do already have +one such test in our Visor tests.) + +#### A: Specific examples available in Jupyter notebooks + +For convenience, the native Python equivalents of the Visor CLI examples above are illustrated in the following notebook. +(Note that this is to illustrate the usage in native Python, not suggesting these need to be automated in a notebook.) + +``` +tests/docs/VisorPythonClassExamples.ipynb +``` + +To test `update_variables` using the native Python class specifically (which has a slightly more involved setup), +a Jupyter notebook can be referenced, included in the 'examples' directory of the Visor repository: + +``` +examples/notebooks/Variable Update Testing ??? Plate Example.ipynb +``` + +#### B: Additional tests for native Python Visor class +In addition to the tests outlined above, we may also want to consider the following: +2. Basic start/stop usage with a VTK object instead of a file path +2. Argument check for the `Visor` class methods +3. Mirror API checks: + * update + * add_dataset + * remove_datsaet + * clear (only Python Native) + * list_variables + * update_variables + * save_state/load_state +4. Async start (`examples/asynchronous_start.py` for reference) + + + + +### 4. Dash Component Features + +The Dash component has specific behaviours that need testing independently of the rest of Visor: + +1. Parts snapshot +2. Aspect ratio and pixel density +3. Dark mode + +TODO: Add any additional details for Dash-specific features that need to be checked on changes. diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 00000000..f2341665 --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,201 @@ +# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited. + +# ==================================================== [Imports] ==================================================== # + +import time +from datetime import datetime, timezone +from multiprocessing import Process +from pathlib import Path +from urllib.parse import urlparse + +import pytest +import requests + +from ansys.visor.viewer import Visor +from tests.helpers.utils.dash_server import run_dash_server +from tests.helpers.utils.ports import reserved_port + + +@pytest.fixture(scope="module") +def visor_asset_spec(request): + """ + Carries the asset + metadata paths. If not parametrized, uses defaults. + + Specify in test via: + @pytest.mark.parametrize( + "visor_asset_spec", + [{ + "asset_path": Path("path/to/asset.vtm"), + "metadata_path": Path("path/to/metadata.json"), + }], + indirect=True, + """ + default = { + "asset_path": str(Path("examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm")), + "metadata_path": str(Path("examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json")), + "dark_mode": None, + } + return getattr(request, "param", default) + + +@pytest.fixture(scope="module") +def visor_server(request, visor_asset_spec): + """ + Starts Visor server via Python API, launches visualizer with asset + metadata, + and stops it after tests. + + Important: One instance per (session x asset_spec parameter) + """ + # Getting URL from pytest option: ----------------------- + # It would be nice to allow the tester to use a specific url or port if they really wanted to despite possibly not being available + + # url = request.config.getoption("--url") + # visor_port = request.config.getoption("--visor_port") + #-------------------------------------------------------- + host = request.config.getoption("--host") + + # Asset and metadata paths are now variables + asset_path = visor_asset_spec["asset_path"] + metadata_path = visor_asset_spec["metadata_path"] + dark_mode = visor_asset_spec.get("dark_mode", None) + + with reserved_port(host=host) as (host, port, guard): + base_url = f"http://{host}:{port}" + url = f"{base_url}/index.html" + + try: + guard.close() + except Exception: + pass + + visor = Visor(url=url, dark_mode=dark_mode) + visor.start(asset_path, metadata=metadata_path) + + # Poll the target URL until it's reachable (HTTP 200) + deadline = time.time() + 45 + last_err = None + while time.time() < deadline: + try: + resp = requests.get(url, timeout=2) + if resp.status_code == 200: + break + except Exception as e: + last_err = e + time.sleep(1.5) + else: + raise RuntimeError( + f"Visor visualizer not reachable at {url} within the expected time. " + f"Last error: {last_err!r}" + ) + + # Yield the instance for tests + yield visor + + # Stop the server after tests + visor.stop() + +# DASH fixtures ----------------------------- + +@pytest.fixture(scope="module") +def launch_dash_server(request, visor_server): + """ + Starts a Dash app that embeds the Visordash component connected to the Visor instance. + Waits for it to be reachable, yields its base URL, and tears it down afterwards. + + Depends on `visor_server` to ensure Visor is up before Dash starts. + """ + # Parse Visor host/port from visor.url (default: http://localhost:8081/index.html) + parsed = urlparse(visor_server.url) + + if ":" in parsed.netloc: + visor_host, port_str = parsed.netloc.split(":", 1) + visor_port = int(port_str) + else: + visor_host = parsed.hostname or "localhost" + visor_port = parsed.port or 8081 + + with reserved_port(host=visor_host) as (host, dash_port, guard): + base_url = f"http://{host}:{dash_port}" + dash_url = f"{base_url}/index.html" + + try: + guard.close() + except Exception: + pass + + # Optional prop overrides supplied via indirect parametrize + dash_props = getattr(request, "param", {}) + + # Start Dash in a separate process for clean teardown + proc = Process( + target=run_dash_server, + args=(visor_host, visor_port, dash_port), + kwargs=dash_props, + ) + proc.daemon = True + proc.start() + + # Poll Dash until reachable (HTTP 200) + deadline = time.time() + 90 + last_err = None + while time.time() < deadline: + try: + resp = requests.get(dash_url, timeout=2) + if resp.status_code == 200: + break + except Exception as e: + last_err = e + time.sleep(1.5) + else: + # Ensure we terminate the process if startup failed + try: + proc.terminate() + except Exception: + pass + raise RuntimeError( + f"Dash app not reachable at {dash_url} within the expected time. " + f"Last error: {last_err!r}" + ) + + # Yield the Dash URL for tests + yield dash_url + + # Stop Dash after tests + try: + proc.terminate() + proc.join(timeout=10) + finally: + if proc.is_alive(): + # Force kill if still running + proc.kill() + + +# =================================================== [Playwright] =================================================== # +# This fixture automatically starts Playwright tracing for each test, and if the test fails, it saves the trace to a file for later analysis. + +@pytest.fixture(autouse=True) +def trace_on_failure(request, page): + # start tracing for this test + page.context.tracing.start(screenshots=True, snapshots=True, sources=True) + yield + # after test: if failed -> stop and save trace + rep = request.node.rep_call # set by pytest_runtest_makereport hook below + if rep and rep.failed: + outdir = Path(request.config.rootpath) / "artifacts" / "traces" + outdir.mkdir(parents=True, exist_ok=True) + ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + fname = outdir / f"{request.node.name}.{ts}.zip" + page.context.tracing.stop(path=str(fname)) + print(f"Saved Playwright trace to: {fname}") + else: + # stop without saving to avoid disk use + page.context.tracing.stop() + +# This hook captures the test report for the "call" phase (the actual test execution) and attaches it to the test item, so that the trace_on_failure +# fixture can check if the test failed or not after the test runs. +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + rep = outcome.get_result() + if rep.when == "call": + item.rep_call = rep \ No newline at end of file diff --git a/tests/e2e/regressions/test_save_load_state.py b/tests/e2e/regressions/test_save_load_state.py new file mode 100644 index 00000000..e180c191 --- /dev/null +++ b/tests/e2e/regressions/test_save_load_state.py @@ -0,0 +1,181 @@ +# (c)2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited. +"""End-to-end regression tests for the save/load state feature. + +These tests verify that Visor can serialize its viewer state (scene graph, +datasets) to disk and restore it correctly, preserving datasets and keeping +the WebGL canvas functional throughout the round-trip. + +save_state is async (it requests state from the frontend via websocket) so we +schedule it on the Trame background event loop via the server manager's +AsyncRunner. load_state is sync and can be called directly. +""" + +import json +from pathlib import Path + +import pytest + +from tests.helpers.utils.canvas import ( + assert_canvas_alive, + goto_and_wait, + settle, + wait_for_canvas, + wait_for_webgl, +) + + +def _save_state_sync(visor, state_dir: str, force_snapshot: bool = False) -> None: + """Call the async save_state on the Trame event loop, blocking until done. + + Parameters + ---------- + force_snapshot : bool + If True, mark all datasets as dirty so snapshots are always written + to state_dir regardless of prior save history. + """ + if force_snapshot: + for dataset in visor._scene.datasets.values(): + dataset.is_dirty = True + runner = visor._server_manager._async_runner + runner.run_coroutine_block(visor.save_state(state_dir), False) + + +@pytest.mark.regression +class TestSaveLoadState: + """Regression tests for save/load state functionality.""" + + def test_save_state_creates_artifacts(self, visor_server, page, tmp_path): + """save_state should create visor.json and at least one vtkhdf snapshot.""" + state_dir = str(tmp_path / "state") + Path(state_dir).mkdir() + + # Frontend must be connected for save_state to retrieve state via websocket + goto_and_wait(page, visor_server.url) + wait_for_canvas(page) + wait_for_webgl(page) + + _save_state_sync(visor_server, state_dir) + + state_file = Path(state_dir) / "visor.json" + assert state_file.exists(), "visor.json was not created by save_state" + + content = json.loads(state_file.read_text()) + assert isinstance(content, dict), "visor.json should contain a JSON object" + assert "version" in content, "visor.json should contain a version field" + + vtkhdf_files = list(Path(state_dir).glob("*.vtkhdf")) + assert len(vtkhdf_files) >= 1, "Expected at least one .vtkhdf snapshot file" + + def test_save_load_roundtrip_datasets_preserved(self, visor_server, page, tmp_path): + """Datasets should be preserved across a save/load round-trip.""" + state_dir = str(tmp_path / "state") + Path(state_dir).mkdir() + + # Frontend must be connected for save_state + goto_and_wait(page, visor_server.url) + wait_for_canvas(page) + wait_for_webgl(page) + + datasets_before = visor_server.list_datasets() + names_before = {ds["name"] for ds in datasets_before.values()} + assert len(names_before) > 0, "Expected at least one dataset loaded initially" + + _save_state_sync(visor_server, state_dir) + visor_server.load_state(state_dir) + + datasets_after = visor_server.list_datasets() + names_after = {ds["name"] for ds in datasets_after.values()} + + assert names_before == names_after, ( + f"Dataset names changed after round-trip: before={names_before}, after={names_after}" + ) + + def test_save_load_canvas_still_alive(self, visor_server, page, tmp_path): + """Canvas should remain functional after save and load operations.""" + url = visor_server.url + state_dir = str(tmp_path / "state") + Path(state_dir).mkdir() + + goto_and_wait(page, url) + wait_for_canvas(page) + wait_for_webgl(page) + assert_canvas_alive(page, "before save_state") + + _save_state_sync(visor_server, state_dir) + settle(page, ms=300) + assert_canvas_alive(page, "after save_state") + + visor_server.load_state(state_dir) + settle(page, ms=500) + assert_canvas_alive(page, "after load_state") + + def test_save_load_state_file_content_valid(self, visor_server, page, tmp_path): + """The state file should contain valid structure with expected fields.""" + state_dir = str(tmp_path / "state") + Path(state_dir).mkdir() + + # Frontend must be connected for save_state + goto_and_wait(page, visor_server.url) + wait_for_canvas(page) + wait_for_webgl(page) + + _save_state_sync(visor_server, state_dir) + + state_file = Path(state_dir) / "visor.json" + content = json.loads(state_file.read_text()) + + # Verify top-level structure + assert content.get("version") == "1.0", "State version should be 1.0" + assert "scene" in content, "State should contain scene data" + assert "dataset_states" in content["scene"], "Scene should contain dataset_states" + assert len(content["scene"]["dataset_states"]) > 0, ( + "dataset_states should not be empty" + ) + + # Verify each dataset state references a snapshot path + for name, ds_state in content["scene"]["dataset_states"].items(): + snapshot_path = ds_state.get("serialized_dataset_path") + assert snapshot_path is not None, ( + f"Dataset '{name}' missing serialized_dataset_path" + ) + assert len(snapshot_path) > 0, ( + f"Dataset '{name}' has empty serialized_dataset_path" + ) + + def test_load_state_into_empty_scene(self, visor_server, page, tmp_path): + """Loading state into an empty scene should restore datasets from snapshots. + + NOTE: This test must run last because it removes all datasets + before restoring them. + """ + state_dir = str(tmp_path / "state") + Path(state_dir).mkdir() + + # Frontend must be connected for save_state + goto_and_wait(page, visor_server.url) + wait_for_canvas(page) + wait_for_webgl(page) + + # Save current state (with dataset) - force_snapshot ensures .vtkhdf files + # are written even if a prior test's save marked datasets clean + _save_state_sync(visor_server, state_dir, force_snapshot=True) + + # Remove all datasets to empty the scene + dataset_ids = list(visor_server.list_datasets().keys()) + for dataset_id in dataset_ids: + visor_server.remove_dataset(dataset_id) + + empty_datasets = visor_server.list_datasets() + assert len(empty_datasets) == 0, "Scene should be empty after removing all datasets" + + # Restore from saved state - load_state should restore from vtkhdf snapshots + visor_server.load_state(state_dir) + + restored_datasets = visor_server.list_datasets() + assert len(restored_datasets) > 0, "Datasets should be restored after load_state" + + # Verify canvas is functional with restored scene + goto_and_wait(page, visor_server.url) + wait_for_canvas(page) + wait_for_webgl(page, timeout_ms=15_000) + assert_canvas_alive(page, "after load_state into empty scene") diff --git a/tests/e2e/regressions/test_simple_dash.py b/tests/e2e/regressions/test_simple_dash.py new file mode 100644 index 00000000..41fb4136 --- /dev/null +++ b/tests/e2e/regressions/test_simple_dash.py @@ -0,0 +1,140 @@ +# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited. + +import json +from pathlib import Path + +import pytest +from deepdiff import DeepDiff +from playwright.sync_api import Page + +from tests.helpers import visual +from tests.helpers.comparison.image_diff import compare_images +from tests.helpers.utils.canvas import settle, wait_for_canvas, wait_for_webgl + + +@pytest.mark.regression +def test_dash_start(page: Page, launch_dash_server, baseline_dir, request, suite_name): + """ + Regression test for DASH startup and basic geometry loading + + """ + # add launch_dash_server fixture to ensure server + instance are running + target_url = launch_dash_server # usually http://localhost:8050/index.html" + + page.set_viewport_size({"width": 1280, "height": 800}) + + # Navigate and wait for network idle + page.goto(target_url, wait_until="domcontentloaded") + + # Wait for the canvas and WebGL + canvas = wait_for_canvas(page) + wait_for_webgl(page) + settle(page) + + verify_comparison_1 = visual.verify_canvas_against_baseline( + canvas_locator=canvas, + baseline_dir=baseline_dir, + pixel_threshold=2.55, + request=request, + compare_images=compare_images, + test_suite=suite_name, + test_id="dash_start", + ) + + # compile results (we only have one test though) and assert + result = visual.make_test_result(verify_comparison_1) + + assert result.passed, "Visual comparison failed\t\n" + result.summary() + print(result.summary()) + +@pytest.mark.xfail(reason="Under investigation, issue #983, remove when fixed", strict=False) +@pytest.mark.regression +def test_dash_snapshot(page: Page, launch_dash_server, baseline_dir, request, suite_name): + """ + Regression test for "Get Parts Snapshot" in dash + + """ + # add launch_dash_server fixture to ensure server + instance are running + target_url = launch_dash_server # usually http://localhost:8050/index.html" + + page.set_viewport_size({"width": 1280, "height": 800}) + + # Navigate and wait for network idle + page.goto(target_url, wait_until="domcontentloaded") + + # Wait for the canvas and WebGL + wait_for_canvas(page) + wait_for_webgl(page) + settle(page) + + # Wait for the snapshot button to appear and be visible + snapshot_btn = page.locator("#snapshot-btn") + snapshot_btn.wait_for(state="visible", timeout=15_000) + + # Click the button to produce the JSON output + snapshot_btn.click() + + # Wait until the output
 contains non-null, non-empty text
+    page.wait_for_function(
+        """
+        () => {
+        const out = document.querySelector('#output');
+        if (!out) return false;
+        const txt = out.innerText || out.textContent || '';
+        return txt.trim().length > 0 && txt.trim() !== 'null';
+        }
+        """,
+        timeout=30_000,
+    )
+
+    # Small pause to ensure any final rendering is done
+    page.wait_for_timeout(200)
+
+    # Read the output text
+    output_locator = page.locator("#output")
+    output_text = output_locator.text_content()
+    assert output_text is not None, "Output element returned None for text_content()"
+
+    # Output saving to upload as package
+    out_dir = Path("tests/artifacts", suite_name , "test_dash_snapshot")
+    out_dir.mkdir(parents=True, exist_ok=True)
+
+    # assert it's valid JSON and not "null"
+    try:
+        output_json = json.loads(output_text)
+    except Exception as e:
+        # Save raw output for debugging
+        actual_path = Path(out_dir) / "dash_snapshot_actual_invalid.json"
+        actual_path.write_text(output_text, encoding="utf-8")
+        raise AssertionError(f"Output is not valid JSON: {e}. Raw output saved to {actual_path}") from e
+
+    # Save the actual JSON to a file for inspection / CI artifacts
+    actual_path = Path(out_dir) / "dash_snapshot_actual.json"
+    actual_path.write_text(json.dumps(output_json, indent=2), encoding="utf-8")
+
+    # Compare to a reference file in baseline_dir
+    reference_path = Path(baseline_dir) / "dash_snapshot_reference.json"
+    update_baseline = request.config.getoption("--update-baseline")
+
+    if update_baseline or not reference_path.exists():
+       # If no reference exists, write one and fail so CI or developer can review
+        reference_path.write_text(json.dumps(output_json, indent=2), encoding="utf-8")
+        pytest.skip("Baseline updated; re-run without --update-baseline to validate.")
+
+    # Load reference and compare
+    with reference_path.open("r", encoding="utf-8") as file:
+        reference_json = json.load(file)
+
+    diff = DeepDiff(reference_json,
+                    output_json,
+                    exclude_regex_paths=[
+                        r"root\['scene'\]\['datasetStates'\]\['vtk_scene_sphere_l2_b3_r32_v3_c1_z3'\](\[.*\])*\['id'\]"
+                    ],
+                    ignore_order=True)
+    if diff:
+        # Save diff for debugging
+        diff_path = Path(out_dir) / "dash_snapshot_diff.json"
+        diff_path.write_text(json.dumps(diff, indent=2), encoding="utf-8")
+        pytest.fail(f"JSON differs:\n{diff}")
+
+    # assert output_json == reference_json, f"Snapshot JSON does not match reference. Actual saved to {actual_path}; reference is {reference_path}."
diff --git a/tests/e2e/regressions/test_update_variables.py b/tests/e2e/regressions/test_update_variables.py
new file mode 100644
index 00000000..fc19b1da
--- /dev/null
+++ b/tests/e2e/regressions/test_update_variables.py
@@ -0,0 +1,98 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+import numpy as np
+import pytest
+
+from ansys.visor.viewer import Metadata
+from tests.helpers.utils.mesh_creator import MeshCreator
+
+# Random mesh will be created before the visor_server starts to feed it as a parametrized
+# value to the fixture.
+
+# Define the names and number of components for the vector variable to be updated
+TEST_VECTOR_NAME = "test_vector"
+NUM_COMPONENTS = 3
+TEST_SCALAR_NAME = "test_scalar"
+SCALE_FACTOR = 5.0
+
+# Create initial mesh with variables
+# Set up first mesh
+data_obj1 = MeshCreator(xoffset=-2)
+
+# Add the variables
+data_obj1.add_random_variable(
+    TEST_VECTOR_NAME, NUM_COMPONENTS, scale_factor=SCALE_FACTOR
+)
+data_obj1.add_random_variable(TEST_SCALAR_NAME, 1, scale_factor=SCALE_FACTOR)
+
+# Retrieve the polydata object
+polydata1 = data_obj1.polydata
+
+ASSETS = {
+    "asset_path": polydata1,
+    "metadata_path": Metadata(name="sphere_1", unit="m"),
+}
+
+
+@pytest.mark.parametrize("visor_asset_spec", [ASSETS], indirect=True)
+@pytest.mark.regression
+def test_update_variables(visor_server, request):
+    """Test updating_variables() method in a Visor scene
+
+    Creates a random mesh with point variables, loads it into Visor,
+    updates the variable values to constant values, and verifies the update took place.
+
+    Test replicates example in documentation:
+    https://vigilant-lamp-162kw9z.pages.github.io/version/stable/examples/03-updating-theia-scene
+    """
+
+    visor = visor_server
+    # Print dataset metadata and get the dataset ID
+    datasets = visor.list_datasets()
+
+    # Get the dataset ID - there is only one loaded.
+    dataset_id = list(datasets.keys())[0]
+
+    # Get the variable data arrays from Visor (returns per-part entries)
+    variable_data1 = visor.list_variables(dataset_id)
+    # flatten to list of individual variables for easier assertions
+    flat_vars1 = [v for part in variable_data1 for v in getattr(part, "variables", [])]
+
+    # Now update the variable values in Visor
+
+    # Create the new variables for the test point dataArrays:
+    # a list of random values between 0 and 10
+    constant_values = [1.0, 2.0, 3.5]
+    new_vector_values = data_obj1.get_constant_values(NUM_COMPONENTS, constant_values)
+    new_scalar_values = data_obj1.get_constant_values(1, [5.0])
+
+    # Compile a dict with the vector variable metadtata + updated values
+    vector_update_info = {
+        "type": "point",
+        "name": TEST_VECTOR_NAME,
+        "num_components": NUM_COMPONENTS,
+        "data": new_vector_values,
+    }
+    # Compile a dict with the scalar variable metadtata + updated values
+    scalar_update_info = {
+        "type": "point",
+        "name": TEST_SCALAR_NAME,
+        "num_components": 1,
+        "data": new_scalar_values,
+    }
+
+    # Run the actual update command
+    visor.update_variables(dataset_id, [vector_update_info, scalar_update_info])
+    variable_data2 = visor.list_variables(dataset_id)
+    flat_vars2 = [v for part in variable_data2 for v in getattr(part, "variables", [])]
+
+    assert flat_vars1 != flat_vars2, "Failed to update variable data."
+
+    # Find the updated vector variable by name and verify its ranges reflect the constant values
+    updated_vector = [v for v in flat_vars2 if getattr(v, "name", None) == TEST_VECTOR_NAME]
+    assert len(updated_vector) >= 1, f"Updated vector {TEST_VECTOR_NAME} not found in variables"
+    updated_constant_data = updated_vector[0].ranges
+    for i in range(len(updated_constant_data)):
+        assert (
+            float(np.mean(updated_constant_data[i])) == constant_values[i]
+        ), f"Failed to update variable {TEST_VECTOR_NAME}. {float(np.mean(updated_constant_data[i]))}!={constant_values[i]}"
diff --git a/tests/e2e/regressions/test_variable_components.py b/tests/e2e/regressions/test_variable_components.py
new file mode 100644
index 00000000..d30787db
--- /dev/null
+++ b/tests/e2e/regressions/test_variable_components.py
@@ -0,0 +1,288 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+# E2E tests for variable component selection in the viewer UI, focused on 9-component tensor variables.
+
+import time
+from pathlib import Path
+
+import pytest
+from PIL import Image
+from playwright.sync_api import Page
+
+from ansys.visor.viewer import Metadata
+from tests.helpers import visual
+from tests.helpers.comparison.image_diff import compare_images, ensure_same_size_and_mode, rms_diff
+from tests.helpers.utils.canvas import settle, wait_for_canvas_alive
+from tests.helpers.utils.mesh_creator import MeshCreator
+
+# ===========================================================================
+# Test Data Setup: Create mesh with scalar (1-comp), vector (3-comp), and
+# tensor (9-comp) variables using MeshCreator.
+# ===========================================================================
+
+TENSOR_NAME = "Stress"
+VECTOR_NAME = "Velocity"
+SCALAR_NAME = "Temperature"
+SCALE_FACTOR = 10.0
+
+# Build mesh with all three variable types
+data_obj = MeshCreator(scale=1.0, xoffset=0.0)
+data_obj.add_random_variable(SCALAR_NAME, 1, scale_factor=SCALE_FACTOR)
+data_obj.add_random_variable(VECTOR_NAME, 3, scale_factor=SCALE_FACTOR)
+data_obj.add_random_variable(TENSOR_NAME, 9, scale_factor=SCALE_FACTOR)
+
+TENSOR_ASSETS = {
+    "asset_path": data_obj.polydata,
+    "metadata_path": Metadata(name="tensor_test_sphere", unit="m"),
+}
+
+# Expected component labels for the 9-component tensor
+TENSOR_COMPONENT_LABELS = ["XX", "XY", "XZ", "YX", "YY", "YZ", "ZX", "ZY", "ZZ"]
+
+
+# ===========================================================================
+# Variable dropdown shows all 9 tensor components
+# ===========================================================================
+
+@pytest.mark.parametrize("visor_asset_spec", [TENSOR_ASSETS], indirect=True)
+@pytest.mark.regression
+class TestTensorVariableDropdown:
+    """
+    Verify that the variable/spectrum dropdown in the UI contains
+    all 9 tensor components plus Magnitude (10 total) when a dataset with
+    a 9-component variable is loaded.
+    """
+
+    def test_tensor_variable_listed_via_python_api(self, visor_server):
+        """The Python API list_variables returns a 9-component variable."""
+        visor = visor_server
+        datasets = visor.list_datasets()
+        dataset_id = list(datasets.keys())[0]
+
+        variables = visor.list_variables(dataset_id)
+        # list_variables now returns one VisorPartVariables per part; flatten to get individual variables
+        all_vars = [v for part in variables for v in getattr(part, "variables", [])]
+        tensor_vars = [v for v in all_vars if getattr(v, "num_components", None) == 9]
+        assert len(tensor_vars) >= 1, (
+            f"No 9-component variable found. Variables: "
+            f"{[(v.name, v.num_components) for v in variables]}"
+        )
+
+    def test_canvas_loads_with_tensor_data(self, page: Page, visor_server):
+        """The viewer canvas loads and is alive with tensor data."""
+        target_url = visor_server.url
+        page.set_viewport_size({"width": 1280, "height": 800})
+        page.goto(target_url, wait_until="networkidle")
+        canvas = wait_for_canvas_alive(page, "tensor-load")
+        assert canvas is not None
+
+    def test_tensor_component_dropdown_options(self, page: Page, visor_server, baseline_dir, request):
+        """Component select shows all 9 tensor components + Magnitude when Stress is selected."""
+        target_url = visor_server.url
+        page.set_viewport_size({"width": 1280, "height": 800})
+        page.goto(target_url, wait_until="networkidle")
+        wait_for_canvas_alive(page, "tensor-dropdown")
+
+        # Select the part to activate the variable selection UI
+        part_row = page.locator("div.visor-tree-view td:nth-child(2)").filter(has_text="tensor_test_sphere")
+        part_row.wait_for(state="visible", timeout=10_000)
+        part_row.click()
+
+        # Select the Stress (9-component tensor) variable from the variable dropdown.
+        # Value format is "{type}::{name}::{numComponents}" โ€” stable across sessions.
+        variable_select = page.locator("select").filter(has=page.locator("option[data-name='Stress']"))
+        variable_select.wait_for(state="visible", timeout=10_000)
+        variable_select.select_option(value="POINT::Stress::9")
+
+        # Wait for the component dropdown to become visible
+        component_select = page.locator("label").filter(has_text="Component").locator("select")
+        component_select.wait_for(state="visible", timeout=10_000)
+
+        # Assert all 10 options are present: Magnitude + 9 tensor components
+        expected_options = ["Magnitude"] + TENSOR_COMPONENT_LABELS
+        option_texts = component_select.evaluate(
+            "el => Array.from(el.options).map(o => o.text)"
+        )
+        assert option_texts == expected_options, (
+            f"Component dropdown options mismatch.\n"
+            f"  Expected: {expected_options}\n"
+            f"  Got:      {option_texts}"
+        )
+
+        # Screenshot the closed component select element for visual regression
+        result = visual.verify_canvas_against_baseline(
+            canvas_locator=component_select,
+            baseline_dir=baseline_dir,
+            pixel_threshold=11,
+            request=request,
+            compare_images=compare_images,
+            test_id="e2e01_tensor_component_dropdown",
+            test_suite="regression",
+        )
+        suite_result = visual.make_test_result(result)
+        assert suite_result.passed, (
+            "Component dropdown screenshot baseline failed\n" + suite_result.summary()
+        )
+
+
+# ===========================================================================
+# Selecting a tensor component colors the part (screenshot diff)
+# ===========================================================================
+
+@pytest.mark.parametrize("visor_asset_spec", [TENSOR_ASSETS], indirect=True)
+@pytest.mark.regression
+class TestTensorComponentColoring:
+    """
+    Verify that selecting a tensor component updates the rendered
+    view, confirmed by screenshot baseline comparison.
+    """
+
+    def test_initial_render_baseline(self, page: Page, visor_server, baseline_dir, request):
+        """Take a baseline screenshot of the tensor mesh initial render."""
+        target_url = visor_server.url
+        page.set_viewport_size({"width": 1280, "height": 800})
+        page.goto(target_url, wait_until="networkidle")
+        canvas = wait_for_canvas_alive(page, "tensor-initial")
+        settle(page, 300)  # preserve prior render-settle timing before baseline capture
+
+        result = visual.verify_canvas_against_baseline(
+            canvas_locator=canvas,
+            baseline_dir=baseline_dir,
+            pixel_threshold=2.55,
+            request=request,
+            compare_images=compare_images,
+            test_id="e2e02_tensor_initial_render",
+            test_suite="regression",
+        )
+
+        suite_result = visual.make_test_result(result)
+        assert suite_result.passed, "Initial render baseline failed\n" + suite_result.summary()
+
+    def test_component_selection_changes_rendering(self, page: Page, visor_server):
+        """Selecting different tensor components produces visually distinct canvas renders."""
+        target_url = visor_server.url
+        page.set_viewport_size({"width": 1280, "height": 800})
+        page.goto(target_url, wait_until="networkidle")
+        canvas = wait_for_canvas_alive(page, "tensor-coloring")
+
+        # Select the part to activate the variable selection UI
+        part_row = page.locator("div.visor-tree-view td:nth-child(2)").filter(has_text="tensor_test_sphere")
+        part_row.wait_for(state="visible", timeout=10_000)
+        part_row.click()
+
+        # Select the Stress variable
+        variable_select = page.locator("select").filter(has=page.locator("option[data-name='Stress']"))
+        variable_select.wait_for(state="visible", timeout=10_000)
+        variable_select.select_option(value="POINT::Stress::9")
+
+        # Wait for the component dropdown to become visible
+        component_select = page.locator("label").filter(has_text="Component").locator("select")
+        component_select.wait_for(state="visible", timeout=10_000)
+
+        # Capture a canvas screenshot per component for the representative subset
+        components_to_test = ["Magnitude", "XX", "YY", "ZZ"]
+        screenshots_root = Path("tests/artifacts/regression/screenshots")
+        screenshots_root.mkdir(parents=True, exist_ok=True)
+
+        stamp = time.strftime("%Y%m%d-%H%M%S")
+        shot_paths: list[Path] = []
+        for comp in components_to_test:
+            component_select.select_option(label=comp)
+            settle(page, 500)  # Allow renderer time to update
+            shot_path = screenshots_root / f"e2e04_coloring_{comp}_{stamp}.png"
+            canvas.screenshot(path=str(shot_path))
+            shot_paths.append(shot_path)
+
+        # Assert each consecutive pair of screenshots differs (RMS > threshold),
+        # confirming that selecting a different component changes the rendering.
+        min_rms = 1.0
+        for i in range(len(shot_paths) - 1):
+            img_a = Image.open(shot_paths[i])
+            img_b = Image.open(shot_paths[i + 1])
+            img_a, img_b = ensure_same_size_and_mode(img_a, img_b)
+            rms, _ = rms_diff(img_a, img_b)
+            assert rms > min_rms, (
+                f"Switching from '{components_to_test[i]}' to '{components_to_test[i + 1]}' "
+                f"did not change the render (RMS={rms:.3f}, expected > {min_rms}). "
+                f"Screenshots: {shot_paths[i]}, {shot_paths[i + 1]}"
+            )
+
+
+# ===========================================================================
+# Regression โ€” Selecting scalar/vector components still works
+# ===========================================================================
+
+@pytest.mark.parametrize("visor_asset_spec", [TENSOR_ASSETS], indirect=True)
+@pytest.mark.regression
+class TestScalarVectorRegression:
+    """
+    Confirm that selecting scalar and vector variables still works
+    correctly alongside 9-component tensor variables in the same dataset.
+    """
+
+    def test_all_variable_types_listed(self, visor_server):
+        """All variable types (scalar, vector, tensor) are listed via Python API."""
+        visor = visor_server
+        datasets = visor.list_datasets()
+        dataset_id = list(datasets.keys())[0]
+
+        variables = visor.list_variables(dataset_id)
+        all_vars = [v for part in variables for v in getattr(part, "variables", [])]
+        component_counts = {getattr(v, "num_components", None) for v in all_vars}
+
+        # Expect scalar (1), vector (3), and tensor (9)
+        assert 1 in component_counts, "Scalar (1-comp) variable not found"
+        assert 3 in component_counts, "Vector (3-comp) variable not found"
+        assert 9 in component_counts, "Tensor (9-comp) variable not found"
+
+    def test_scalar_variable_properties(self, visor_server):
+        """Scalar variable has expected properties."""
+        visor = visor_server
+        datasets = visor.list_datasets()
+        dataset_id = list(datasets.keys())[0]
+
+        variables = visor.list_variables(dataset_id)
+        all_vars = [v for part in variables for v in getattr(part, "variables", [])]
+        scalar_vars = [v for v in all_vars if getattr(v, "num_components", None) == 1]
+        assert len(scalar_vars) >= 1
+
+        scalar_var = scalar_vars[0]
+        assert scalar_var.name == SCALAR_NAME
+        assert len(scalar_var.ranges) == 1
+
+    def test_vector_variable_properties(self, visor_server):
+        """Vector variable has expected properties."""
+        visor = visor_server
+        datasets = visor.list_datasets()
+        dataset_id = list(datasets.keys())[0]
+
+        variables = visor.list_variables(dataset_id)
+        all_vars = [v for part in variables for v in getattr(part, "variables", [])]
+        vector_vars = [v for v in all_vars if getattr(v, "num_components", None) == 3 and getattr(v, "name", None) == VECTOR_NAME]
+        assert len(vector_vars) >= 1
+
+        vector_var = vector_vars[0]
+        assert vector_var.name == VECTOR_NAME
+        assert len(vector_var.ranges) == 3
+
+    def test_tensor_variable_properties(self, visor_server):
+        """Tensor variable has expected properties."""
+        visor = visor_server
+        datasets = visor.list_datasets()
+        dataset_id = list(datasets.keys())[0]
+
+        variables = visor.list_variables(dataset_id)
+        all_vars = [v for part in variables for v in getattr(part, "variables", [])]
+        tensor_vars = [v for v in all_vars if getattr(v, "num_components", None) == 9]
+        assert len(tensor_vars) >= 1
+
+        tensor_var = tensor_vars[0]
+        assert tensor_var.name == TENSOR_NAME
+        assert len(tensor_var.ranges) == 9
+
+    def test_canvas_alive_with_mixed_variables(self, page: Page, visor_server):
+        """Canvas loads correctly with scalar, vector, and tensor variables."""
+        target_url = visor_server.url
+        page.set_viewport_size({"width": 1280, "height": 800})
+        page.goto(target_url, wait_until="networkidle")
+        canvas = wait_for_canvas_alive(page, "mixed-variables")
+        assert canvas is not None
diff --git a/tests/e2e/smoke/test_smoke.py b/tests/e2e/smoke/test_smoke.py
new file mode 100644
index 00000000..3666a2a6
--- /dev/null
+++ b/tests/e2e/smoke/test_smoke.py
@@ -0,0 +1,82 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+
+import pytest
+from playwright.sync_api import Page
+
+from tests.helpers import visual
+from tests.helpers.comparison.image_diff import compare_images
+from tests.helpers.utils.canvas import assert_canvas_alive, settle
+
+
+@pytest.mark.smoke
+def test_st03_basic_render_rotation(page: Page, visor_server, baseline_dir, pixel_threshold, request):
+
+    """
+    Smoke: Validate the end-to-end render and rotation interaction  (rotate)
+
+    The test asserts that:
+      - The viewer loads and the WebGL canvas is alive throughout.
+      - Two screenshots are taken and validated: initial load and after rotation.
+    """
+
+    target_url = visor_server.url  # "usually http://localhost:8081/index.html"
+
+    page.set_viewport_size({"width": 1280, "height": 800})
+    page.goto(target_url, wait_until="networkidle")
+
+    console_errors = []
+    def _on_console(msg):
+        # Collect only error-level messages for smoke health checks
+        if msg.type == "error":
+            console_errors.append(msg.text)
+    page.on("console", _on_console)
+
+    page.goto(target_url, wait_until="networkidle")
+
+    canvas = page.locator("canvas.vtk-wasm-1")
+    canvas.wait_for(state="visible", timeout=15_000)
+
+    # Find start point, canvas center
+    box = canvas.bounding_box()
+    assert box is not None, "Canvas bounding box not available"
+    cx = box["x"] + box["width"] / 2
+    cy = box["y"] + box["height"] / 2
+
+    assert_canvas_alive(page, label="initial")
+
+    result_initial = visual.verify_canvas_against_baseline(
+        canvas_locator=canvas,
+        baseline_dir=baseline_dir,
+        pixel_threshold=2.55,
+        request=request,
+        compare_images=compare_images,
+        test_id="st03_canvas_initial",
+        test_suite="smoke",
+    )
+
+    # -------------------------------------------------------------------------
+    # 1) Rotate: left-click + drag diagonally
+    # -------------------------------------------------------------------------
+    page.mouse.move(cx, cy)
+    page.mouse.down(button="left")
+    page.mouse.move(cx + 180, cy - 120)
+    page.mouse.up(button="left")
+    settle(page)
+    assert_canvas_alive(page, label="after-rotate")
+
+    result_rotation = visual.verify_canvas_against_baseline(
+        canvas_locator=canvas,
+        baseline_dir=baseline_dir,
+        pixel_threshold=2.55,
+        request=request,
+        compare_images=compare_images,
+        test_id="st03_canvas_rotation",
+        test_suite="smoke",
+    )
+
+    suite_result = visual.make_test_result(result_initial, result_rotation)
+
+    # Assert once on the aggregate:
+    assert suite_result.passed, "Some checks failed\t\n" + suite_result.summary()
+    print(suite_result.summary())
diff --git a/tests/e2e/smoke/test_smoke_dash_darkmode.py b/tests/e2e/smoke/test_smoke_dash_darkmode.py
new file mode 100644
index 00000000..3514d382
--- /dev/null
+++ b/tests/e2e/smoke/test_smoke_dash_darkmode.py
@@ -0,0 +1,58 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+"""
+Smoke test: verify Visordash renders correctly in dark mode.
+
+This test lives in its own module so that the module-scoped visor_server fixture
+is created with dark_mode=True (the Visor backend must be initialized with dark mode
+enabled for the frontend theme to apply correctly).
+"""
+
+from pathlib import Path
+
+import pytest
+from playwright.sync_api import Page
+
+from tests.helpers.utils.canvas import (
+    assert_canvas_alive,
+    goto_and_wait,
+    settle,
+    wait_for_canvas,
+    wait_for_webgl,
+)
+from tests.helpers.utils.ui_selectors import get_visor_viewer_background
+
+# ---------------------------------------------------------------------------
+# Parametrize visor_asset_spec to enable dark_mode on the Visor server
+# ---------------------------------------------------------------------------
+
+DARK_MODE_ASSET_SPEC = {
+    "asset_path": str(Path("examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.vtm")),
+    "metadata_path": str(Path("examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z3.json")),
+    "dark_mode": True,
+}
+
+
+# ---------------------------------------------------------------------------
+# Test
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize("visor_asset_spec", [DARK_MODE_ASSET_SPEC], indirect=True)
+@pytest.mark.parametrize("launch_dash_server", [{"darkMode": True}], indirect=True)
+@pytest.mark.smoke
+def test_dash_dark_mode_canvas_alive(page: Page, launch_dash_server, request):
+    """Smoke: Visordash renders with dark mode enabled on the Visor server."""
+    dash_url = launch_dash_server
+
+    goto_and_wait(page, dash_url, wait_until="domcontentloaded")
+    wait_for_canvas(page)
+    wait_for_webgl(page)
+    settle(page)
+    assert_canvas_alive(page, label="dash-variant-dark-mode")
+
+    # Assert dark mode background color
+    bg = get_visor_viewer_background(page)
+    assert bg == "54, 60, 67", (
+        f"Expected dark mode viewer background '54, 60, 67', got '{bg}'"
+    )
diff --git a/tests/e2e/smoke/test_smoke_dash_props.py b/tests/e2e/smoke/test_smoke_dash_props.py
new file mode 100644
index 00000000..738c7187
--- /dev/null
+++ b/tests/e2e/smoke/test_smoke_dash_props.py
@@ -0,0 +1,89 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+"""
+Smoke tests: verify Visordash renders with different property configurations.
+
+Each parametrized case launches Visor + Dash with a different set of Visordash
+props (darkMode, aspectRatio, pixelDensity) and asserts that the WebGL canvas
+is alive and asserts values from the DOM that indicate the prop took effect (e.g. background color for
+"""
+
+import pytest
+from playwright.sync_api import Page
+
+from tests.helpers.utils.canvas import (
+    assert_canvas_alive,
+    goto_and_wait,
+    settle,
+    wait_for_canvas,
+    wait_for_webgl,
+)
+from tests.helpers.utils.ui_selectors import (
+    get_container_aspect_ratio,
+    get_ui_panel_scale,
+    get_visor_viewer_background,
+)
+
+# ---------------------------------------------------------------------------
+# Dash properties variant matrix
+# ---------------------------------------------------------------------------
+
+DASH_VARIANTS = [
+    pytest.param(
+        {"darkMode": False},
+        {"check": "background", "expected": "247, 247, 247"},
+        id="light-mode",
+    ),
+    pytest.param(
+        {"aspectRatio": 1.0},
+        {"check": "aspect_ratio", "expected": "1 / 1"},
+        id="aspect-ratio-1.0",
+    ),
+    pytest.param(
+        {"pixelDensity": 750},
+        {"check": "pixel_density", "expected_density": 750},
+        id="pixel-density-750",
+    ),
+]
+
+
+# ---------------------------------------------------------------------------
+# Test
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+    "launch_dash_server, expected", DASH_VARIANTS, indirect=["launch_dash_server"]
+)
+@pytest.mark.smoke
+def test_dash_prop_variant_canvas_alive(page: Page, launch_dash_server, expected, request):
+    """Smoke: Visordash renders with non-default prop configuration."""
+    dash_url = launch_dash_server
+
+    goto_and_wait(page, dash_url, wait_until="domcontentloaded")
+    wait_for_canvas(page)
+    wait_for_webgl(page)
+    settle(page)
+    assert_canvas_alive(page, label=f"dash-variant-{request.node.callspec.id}")
+
+    # Prop-specific DOM assertions
+    check = expected["check"]
+    if check == "background":
+        bg = get_visor_viewer_background(page)
+        assert bg == expected["expected"], (
+            f"Expected viewer background '{expected['expected']}', got '{bg}'"
+        )
+    elif check == "aspect_ratio":
+        ratio = get_container_aspect_ratio(page)
+        assert ratio == expected["expected"], (
+            f"Expected aspect-ratio '{expected['expected']}', got '{ratio}'"
+        )
+    elif check == "pixel_density":
+        scale = get_ui_panel_scale(page)
+        assert scale is not None, "Could not read UI panel transform scale"
+        # scale = containerWidth / pixelDensity; viewport is 1280px (set in goto_and_wait)
+        # Note: top-left panel applies an afterScale factor of 0.85 (see UiScaffold.tsx)
+        expected_scale = (1280 / expected["expected_density"]) * 0.85
+        assert abs(scale - expected_scale) < 0.1, (
+            f"Expected scale ~{expected_scale:.3f}, got {scale:.3f}"
+        )
diff --git a/tests/e2e/smoke/test_smoke_saf_visor.py b/tests/e2e/smoke/test_smoke_saf_visor.py
new file mode 100644
index 00000000..6953b59c
--- /dev/null
+++ b/tests/e2e/smoke/test_smoke_saf_visor.py
@@ -0,0 +1,336 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+"""
+Smoke tests: verify saf-visor-poc loads and visualizes supported VTK file types.
+
+Each parametrized case uploads a file of a given type (.vtp, .vtu) through the
+"VTK Files" page and asserts that the upload succeeds and the Visor viewer
+container is populated.
+
+These tests are skipped automatically when the SAF CLI is not available.
+"""
+
+import os
+import re
+import subprocess
+import tempfile
+import time
+import warnings
+from dataclasses import dataclass
+from pathlib import Path
+
+import pytest
+import requests
+from playwright.sync_api import Page, expect
+
+from ansys.visor.viewer.vtk.io.file_to_dataset import file_to_dataset
+from tests.helpers.utils.ports import reserve_free_port
+from tests.helpers.utils.process import kill_proc_tree
+from tests.helpers.utils.visor_api_tool import discover_from_log, list_datasets, save_state
+
+
+def _saf_cli_available() -> bool:
+    """Check if the SAF CLI is installed by running 'saf --version'."""
+    try:
+        result = subprocess.run(
+            ["saf", "--version"],
+            capture_output=True,
+            timeout=10,
+        )
+        return result.returncode == 0
+    except (FileNotFoundError, subprocess.TimeoutExpired):
+        return False
+
+
+if not _saf_cli_available():
+    pytest.skip("SAF CLI not available (saf --version failed)", allow_module_level=True)
+
+# Console error substrings that originate from TDV WASM initialisation and are benign
+_BENIGN_CONSOLE_PATTERNS = (
+    "appendChild",
+    "addSelectionChangeListener",
+    "style",
+    "rows",
+    "FpsCounter",
+)
+
+# The VISOR launch alert fires once launch_visor()'s long-running transaction
+# completes, but the shared "visor" instance handle used by update_visor() can
+# take a little longer to become visible.
+# These substrings identify that specific transient error
+# so the upload can be retried instead of treated as a real failure.
+_TRANSIENT_INSTANCE_ERROR_MARKERS = (
+    "has not been initialized",
+    "out of sequence",
+)
+
+
+@dataclass
+class SafServerInfo:
+    """SAF server connection info yielded by the saf_server fixture."""
+
+    project_url: str
+    log_path: str
+
+
+# ---------------------------------------------------------------------------
+# Fixtures
+# ---------------------------------------------------------------------------
+
+
+@pytest.fixture(scope="module")
+def saf_server(request):
+    """
+    Launch saf-visor-poc via `saf run` and yield its connection info.
+
+    project_url includes the SAF project ID path (e.g. /projects/), which is
+    required for Dash project-context injection to work. log_path is the SAF
+    stdout log, used to discover the Visor management API host/port (see
+    tests/helpers/utils/visor_api_tool.py).
+
+    Requires either --saf-project-dir CLI option or SAF_VISOR_POC_DIR env var
+    to point at the saf-visor-poc root directory.
+    """
+    saf_project_dir_raw = request.config.getoption("--saf-project-dir") or os.environ.get(
+        "SAF_VISOR_POC_DIR"
+    )
+    if not saf_project_dir_raw:
+        pytest.skip(
+            "SAF project dir not configured; "
+            "pass --saf-project-dir or set SAF_VISOR_POC_DIR env var"
+        )
+
+    saf_project_dir = Path(saf_project_dir_raw).resolve()
+    if not saf_project_dir.is_dir():
+        pytest.skip(f"SAF project dir not found: {saf_project_dir}")
+
+    host = request.config.getoption("--host")
+
+    _, ui_port, ui_guard = reserve_free_port(host=host)
+    _, api_port, api_guard = reserve_free_port(host=host)
+    # Release guard sockets so the SAF server can bind to those ports
+    ui_guard.close()
+    api_guard.close()
+
+    base_url = f"http://{host}:{ui_port}"
+
+    popen_kwargs: dict = {}
+    if os.name == "nt":
+        popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
+    else:
+        popen_kwargs["preexec_fn"] = os.setsid
+
+    # Write to a temp file โ€” avoids pipe-buffer deadlock (SAF emits many
+    # WARNING lines on startup) and lets us extract the project URL from the log.
+    log_file = tempfile.NamedTemporaryFile(
+        mode="w", suffix=".log", prefix="saf_server_", delete=False
+    )
+    log_path = log_file.name
+
+    proc = subprocess.Popen(
+        [
+            "saf", "run", str(saf_project_dir),
+            "--solution-ui-port", str(ui_port),
+            "--solution-api-port", str(api_port),
+        ],
+        stdout=log_file,
+        stderr=subprocess.STDOUT,
+        **popen_kwargs,
+    )
+
+    # Poll until the UI HTTP server responds (else clause fires only on timeout)
+    deadline = time.time() + 90
+    last_err: Exception | None = None
+    while time.time() < deadline:
+        try:
+            resp = requests.get(base_url, timeout=2)
+            if resp.status_code == 200:
+                break
+        except Exception as exc:
+            last_err = exc
+        time.sleep(2)
+    else:
+        log_file.flush()
+        log_file.close()
+        kill_proc_tree(proc.pid)
+        raise RuntimeError(
+            f"SAF UI not reachable at {base_url} within 90s. "
+            f"Last error: {last_err!r}"
+        )
+
+    # SAF logs "INFO - Solution UI: http://host:port/projects/" after startup.
+    # Navigate directly to the project URL so the Dash project context is set.
+    # The Flask app can start accepting connections (HTTP 200 above) slightly
+    # before this log line is written, so poll the log for a few seconds.
+    project_url = None
+    log_deadline = time.time() + 15
+    while time.time() < log_deadline:
+        log_file.flush()
+        with open(log_path) as f:
+            log_content = f.read()
+        match = re.search(r"INFO - Solution UI: (http://\S+)", log_content)
+        if match:
+            project_url = match.group(1)
+            break
+        time.sleep(1)
+
+    if project_url is None:
+        log_file.close()
+        kill_proc_tree(proc.pid)
+        raise RuntimeError(
+            f"Could not find 'Solution UI' project URL in SAF log within 15s. "
+            f"Log path: {log_path}"
+        )
+
+    yield SafServerInfo(project_url=project_url, log_path=log_path)
+
+    kill_proc_tree(proc.pid)
+    try:
+        proc.wait(timeout=10)
+    except subprocess.TimeoutExpired:
+        proc.kill()
+    log_file.close()
+    try:
+        Path(log_path).unlink(missing_ok=True)
+    except PermissionError:
+        pass  # Windows may keep the file locked briefly after process exit
+
+
+# ---------------------------------------------------------------------------
+# Tests
+# ---------------------------------------------------------------------------
+
+# Each entry is (file_path,) the test ID reflects the file extension.
+_VTK_CASES = [
+    pytest.param(Path("tests/files/plate.vtp"), id="vtp"),
+    pytest.param(Path("tests/files/mesh.vtu"), id="vtu"),
+]
+
+
+@pytest.mark.smoke
+@pytest.mark.saf
+@pytest.mark.parametrize("vtk_file_path", _VTK_CASES)
+def test_saf_visor_vtk_filetype_loads(
+    page: Page,
+    saf_server: SafServerInfo,
+    vtk_file_path: Path,
+) -> None:
+    """
+    Smoke: Verify saf-visor-poc loads and visualizes VTP and VTU files.
+
+    Navigates to the VTK Files page, uploads a file, and asserts:
+      - The upload status reports success with the filename.
+      - The Visor viewer container is populated after upload.
+    """
+    vtk_file = vtk_file_path.resolve()
+
+    if not vtk_file.is_file():
+        pytest.skip(f"Test file not found: {vtk_file}")
+
+    page.set_viewport_size({"width": 1280, "height": 800})
+
+    console_errors: list[str] = []
+
+    def _on_console(msg) -> None:
+        is_benign = any(pattern in msg.text for pattern in _BENIGN_CONSOLE_PATTERNS)
+        if msg.type == "error" and not is_benign:
+            console_errors.append(msg.text)
+
+    page.on("console", _on_console)
+
+    page.goto(saf_server.project_url, wait_until="domcontentloaded")
+
+    # The About page auto-launches the Visor service on first load.
+    # We need to wait for the launch result alert before navigating away so that
+    # update_visor() does not fail with "shared product instance not initialized".
+    page.locator("#visor_launch_message .alert").wait_for(state="visible", timeout=120_000)
+
+    # The launch alert fires once launch_visor()'s transaction completes, but the
+    # shared "visor" instance handle can take a little longer to become visible.
+    # The "Visor API host/port" log line itself can also lag slightly behind the alert
+    instance_ready_deadline = time.time() + 60
+    while True:
+        try:
+            api_host, api_port = discover_from_log(saf_server.log_path)
+            list_datasets(api_host, api_port)
+            break
+        except (requests.ConnectionError, RuntimeError):
+            if time.time() >= instance_ready_deadline:
+                pytest.fail("Visor shared instance did not become ready within 60s")
+            time.sleep(2)
+
+    # Click "VTK Files" in the sidebar navigation
+    vtk_nav = page.locator("text=VTK Files")
+    vtk_nav.wait_for(state="visible", timeout=15_000)
+    vtk_nav.click()
+
+    # Confirm the upload button is visible before attempting upload
+    upload_btn = page.locator("#btn_vtk_upload")
+    upload_btn.wait_for(state="visible", timeout=10_000)
+
+    status_locator = page.locator("#vtk_upload_status")
+
+    # Retry on the transient "instance not initialized" race (see marker comment
+    # above) as a safety net, since re-selecting the file re-triggers the upload
+    # callback cleanly.
+    max_attempts = 3
+    for attempt in range(1, max_attempts + 1):
+        page.locator("input[type='file']").set_input_files(str(vtk_file))
+        expect(status_locator).to_contain_text(vtk_file.name, timeout=30_000)
+        status_text = status_locator.inner_text()
+
+        is_transient = any(marker in status_text for marker in _TRANSIENT_INSTANCE_ERROR_MARKERS)
+        if not is_transient or attempt == max_attempts:
+            break
+        page.wait_for_timeout(2_000 * attempt)
+
+    assert "โœ…" in status_text, (
+        f"Upload did not succeed for {vtk_file.name}. Status: {status_text!r}"
+    )
+
+    # # # Currently disabled: SAF-visor poc always show at least one child even on a failed upload
+    # # Assert the Visor viewer container was populated with child elements
+    # viewer = page.locator("#visor_vtk_viewer_container")
+    # viewer.wait_for(state="visible", timeout=10_000)
+    # child_count = viewer.evaluate("el => el.children.length")
+    # assert child_count > 0, "Visor viewer container is empty after upload"
+
+    # Report console errors as warnings but allow test to pass
+    if console_errors:
+        warnings.warn(
+            f"Unexpected JS console errors detected: {console_errors}",
+            UserWarning,
+            stacklevel=2,
+        )
+
+    # Save the visualizer's state and assert the resulting VTKHDF snapshot actually contains geometry.
+
+    state_dir = Path(
+        "tests/artifacts", "saf-smoke", "results", "test_saf_visor_vtk_filetype_loads", vtk_file_path.stem
+    ).resolve()
+    state_dir.mkdir(parents=True, exist_ok=True)
+
+    # Retry save_state briefly since backend registration can lag after upload.
+    save_state_deadline = time.time() + 30
+    while True:
+        try:
+            save_state(api_host, api_port, str(state_dir))
+            break
+        except requests.HTTPError as exc:
+            body = exc.response.text if exc.response is not None else ""
+            if time.time() >= save_state_deadline:
+                pytest.fail(
+                    f"save_state failed with {exc}.\nServer response body:\n{body}"
+                )
+            time.sleep(2)
+
+    vtkhdf_files = list(state_dir.rglob("*.vtkhdf"))
+    assert vtkhdf_files, f"No .vtkhdf snapshot files found under {state_dir}"
+
+    dataset = file_to_dataset(str(vtkhdf_files[0]))
+    assert dataset.GetNumberOfPoints() > 0, (
+        f"Saved VTKHDF snapshot {vtkhdf_files[0].name} contains no points"
+    )
+    assert dataset.GetNumberOfCells() > 0, (
+        f"Saved VTKHDF snapshot {vtkhdf_files[0].name} contains no cells"
+    )
diff --git a/tests/files/cone_r0.5.vtp b/tests/files/cone_r0.5.vtp
new file mode 100644
index 00000000..e02e6de9
Binary files /dev/null and b/tests/files/cone_r0.5.vtp differ
diff --git a/tests/files/many_blocks/many_blocks.vtm b/tests/files/many_blocks/many_blocks.vtm
new file mode 100644
index 00000000..6d51c75b
--- /dev/null
+++ b/tests/files/many_blocks/many_blocks.vtm
@@ -0,0 +1,227 @@
+
+  
+    
+      
+        
+          
+        
+        
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+      
+      
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+          
+        
+        
+      
+      
+        
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+              
+            
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+        
+        
+      
+      
+    
+    
+      
+        
+      
+      
+        
+      
+      
+        
+      
+      
+        
+      
+      
+        
+      
+      
+        
+      
+      
+        
+      
+      
+    
+    
+  
+
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_0_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_0_0.vtp
new file mode 100644
index 00000000..a5381f0c
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_0_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_10_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_10_0.vtp
new file mode 100644
index 00000000..76d639bb
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_10_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_11_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_11_0.vtp
new file mode 100644
index 00000000..210e03dc
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_11_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_12_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_12_0.vtp
new file mode 100644
index 00000000..a4ce8b50
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_12_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_13_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_13_0.vtp
new file mode 100644
index 00000000..63574e51
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_13_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_14_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_14_0.vtp
new file mode 100644
index 00000000..d2829cb4
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_14_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_15_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_15_0.vtp
new file mode 100644
index 00000000..29f66934
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_15_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_16_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_16_0.vtp
new file mode 100644
index 00000000..90ca9ee3
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_16_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_17_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_17_0.vtp
new file mode 100644
index 00000000..b4ea0126
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_17_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_18_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_18_0.vtp
new file mode 100644
index 00000000..f79af140
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_18_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_19_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_19_0.vtp
new file mode 100644
index 00000000..ca3afe1d
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_19_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_1_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_1_0.vtp
new file mode 100644
index 00000000..18850f64
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_1_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_20_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_20_0.vtp
new file mode 100644
index 00000000..53aedd56
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_20_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_21_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_21_0.vtp
new file mode 100644
index 00000000..bc573a17
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_21_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_22_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_22_0.vtp
new file mode 100644
index 00000000..46bbc858
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_22_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_23_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_23_0.vtp
new file mode 100644
index 00000000..a3dfb428
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_23_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_24_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_24_0.vtp
new file mode 100644
index 00000000..9b5d4d49
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_24_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_25_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_25_0.vtp
new file mode 100644
index 00000000..1fa88b83
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_25_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_26_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_26_0.vtp
new file mode 100644
index 00000000..b8024e14
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_26_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_27_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_27_0.vtp
new file mode 100644
index 00000000..85cf933d
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_27_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_28_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_28_0.vtp
new file mode 100644
index 00000000..6ee6b667
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_28_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_29_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_29_0.vtp
new file mode 100644
index 00000000..7aa94835
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_29_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_2_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_2_0.vtp
new file mode 100644
index 00000000..82145100
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_2_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_30_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_30_0.vtp
new file mode 100644
index 00000000..6f83502c
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_30_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_31_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_31_0.vtp
new file mode 100644
index 00000000..8077a0b6
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_31_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_32_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_32_0.vtp
new file mode 100644
index 00000000..46a18792
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_32_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_33_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_33_0.vtp
new file mode 100644
index 00000000..9394aead
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_33_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_34_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_34_0.vtp
new file mode 100644
index 00000000..f5cae124
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_34_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_35_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_35_0.vtp
new file mode 100644
index 00000000..056e2a3b
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_35_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_36_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_36_0.vtp
new file mode 100644
index 00000000..6eec1085
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_36_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_37_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_37_0.vtp
new file mode 100644
index 00000000..4472d712
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_37_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_38_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_38_0.vtp
new file mode 100644
index 00000000..ffa49b73
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_38_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_39_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_39_0.vtp
new file mode 100644
index 00000000..ef8aec2d
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_39_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_3_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_3_0.vtp
new file mode 100644
index 00000000..84932756
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_3_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_40_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_40_0.vtp
new file mode 100644
index 00000000..35f5e37e
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_40_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_41_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_41_0.vtp
new file mode 100644
index 00000000..750c513e
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_41_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_42_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_42_0.vtp
new file mode 100644
index 00000000..df63b9ff
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_42_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_43_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_43_0.vtp
new file mode 100644
index 00000000..351e3240
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_43_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_44_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_44_0.vtp
new file mode 100644
index 00000000..b20a54ac
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_44_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_45_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_45_0.vtp
new file mode 100644
index 00000000..72d29507
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_45_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_46_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_46_0.vtp
new file mode 100644
index 00000000..fe2fb3e1
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_46_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_47_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_47_0.vtp
new file mode 100644
index 00000000..3a4be267
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_47_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_48_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_48_0.vtp
new file mode 100644
index 00000000..61582232
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_48_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_49_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_49_0.vtp
new file mode 100644
index 00000000..61ee1dc0
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_49_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_4_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_4_0.vtp
new file mode 100644
index 00000000..42e47c59
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_4_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_50_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_50_0.vtp
new file mode 100644
index 00000000..a9e1082d
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_50_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_51_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_51_0.vtp
new file mode 100644
index 00000000..650925b6
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_51_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_52_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_52_0.vtp
new file mode 100644
index 00000000..281f4051
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_52_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_53_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_53_0.vtp
new file mode 100644
index 00000000..d5c83950
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_53_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_54_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_54_0.vtp
new file mode 100644
index 00000000..b48e1790
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_54_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_55_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_55_0.vtp
new file mode 100644
index 00000000..b2f53ff1
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_55_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_56_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_56_0.vtp
new file mode 100644
index 00000000..1d416c8c
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_56_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_57_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_57_0.vtp
new file mode 100644
index 00000000..b4f36c8c
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_57_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_58_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_58_0.vtp
new file mode 100644
index 00000000..0f823dcd
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_58_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_59_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_59_0.vtp
new file mode 100644
index 00000000..e2b408f2
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_59_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_5_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_5_0.vtp
new file mode 100644
index 00000000..0f14d6e5
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_5_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_60_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_60_0.vtp
new file mode 100644
index 00000000..50a739ac
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_60_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_61_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_61_0.vtp
new file mode 100644
index 00000000..29f8815a
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_61_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_62_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_62_0.vtp
new file mode 100644
index 00000000..b5341531
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_62_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_63_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_63_0.vtp
new file mode 100644
index 00000000..b6264e29
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_63_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_64_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_64_0.vtp
new file mode 100644
index 00000000..b9ae68a9
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_64_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_65_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_65_0.vtp
new file mode 100644
index 00000000..91f3d322
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_65_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_6_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_6_0.vtp
new file mode 100644
index 00000000..1758199f
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_6_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_7_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_7_0.vtp
new file mode 100644
index 00000000..ed98288d
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_7_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_8_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_8_0.vtp
new file mode 100644
index 00000000..7245bd75
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_8_0.vtp differ
diff --git a/tests/files/many_blocks/many_blocks/many_blocks_9_0.vtp b/tests/files/many_blocks/many_blocks/many_blocks_9_0.vtp
new file mode 100644
index 00000000..81fbf0d3
Binary files /dev/null and b/tests/files/many_blocks/many_blocks/many_blocks_9_0.vtp differ
diff --git a/tests/files/mesh.vtkhdf b/tests/files/mesh.vtkhdf
new file mode 100644
index 00000000..22fd0b2a
Binary files /dev/null and b/tests/files/mesh.vtkhdf differ
diff --git a/tests/files/mesh.vtu b/tests/files/mesh.vtu
new file mode 100644
index 00000000..b8b90a2a
--- /dev/null
+++ b/tests/files/mesh.vtu
@@ -0,0 +1,26 @@
+
+
+  
+    
+      
+        
+          0 0 0
+          1 0 0
+          0 1 0
+          0 0 1
+        
+      
+      
+        
+          0 1 2 3
+        
+        
+          4
+        
+        
+          10
+        
+      
+    
+  
+
diff --git a/tests/files/plate.scdocx b/tests/files/plate.scdocx
new file mode 100644
index 00000000..1038faa0
Binary files /dev/null and b/tests/files/plate.scdocx differ
diff --git a/tests/files/plate.vtkhdf b/tests/files/plate.vtkhdf
new file mode 100644
index 00000000..96ff5249
Binary files /dev/null and b/tests/files/plate.vtkhdf differ
diff --git a/tests/files/plate.vtp b/tests/files/plate.vtp
new file mode 100644
index 00000000..c9f669dc
Binary files /dev/null and b/tests/files/plate.vtp differ
diff --git a/tests/files/sample_hex_cube.vtp b/tests/files/sample_hex_cube.vtp
new file mode 100644
index 00000000..4c1510bc
Binary files /dev/null and b/tests/files/sample_hex_cube.vtp differ
diff --git a/tests/files/simple_multiblock.vtkhdf b/tests/files/simple_multiblock.vtkhdf
new file mode 100644
index 00000000..62ce7afc
Binary files /dev/null and b/tests/files/simple_multiblock.vtkhdf differ
diff --git a/tests/files/tria_cube.vtp b/tests/files/tria_cube.vtp
new file mode 100644
index 00000000..9b90cc27
Binary files /dev/null and b/tests/files/tria_cube.vtp differ
diff --git a/tests/helpers/comparison/image_diff.py b/tests/helpers/comparison/image_diff.py
new file mode 100644
index 00000000..3609c008
--- /dev/null
+++ b/tests/helpers/comparison/image_diff.py
@@ -0,0 +1,45 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+import math
+from pathlib import Path
+
+from PIL import Image, ImageChops, ImageStat
+
+
+def ensure_same_size_and_mode(a: Image.Image, b: Image.Image) -> tuple[Image.Image, Image.Image]:
+    if a.size != b.size:
+        # Resize A to match B to avoid false failures if canvas sizing changes slightly
+        a = a.resize(b.size, Image.Resampling.LANCZOS)
+    # Images work better when converted to RGB
+    a = a.convert("RGB")
+    b = b.convert("RGB")
+
+    return a, b
+
+def rms_diff(a: Image.Image, b: Image.Image):
+    """
+    Compute Root Mean Square difference between two images (0..255).
+    requires tuning
+    """
+    diff = ImageChops.difference(a, b)
+    # Calculate statistics for resultant diff image
+    stat = ImageStat.Stat(diff)
+
+    rms = math.sqrt(sum(s**2 for s in stat.mean) / len(stat.mean))
+    # diff is supplied to be able to upload as a package on failure
+    return rms, diff
+
+def compare_images(current_path: Path, baseline_path: Path, diff_out: Path, threshold: float) -> tuple[bool, float, Path | None]:
+    """
+    Return (passed, rms_value, diff_path_if_fail).
+    """
+    with Image.open(current_path) as curr, Image.open(baseline_path) as base:
+        curr, base = ensure_same_size_and_mode(curr, base)
+        curr = curr.copy()
+        base = base.copy()
+    rms, diff = rms_diff(curr, base)
+    if rms > threshold:
+        diff_out.parent.mkdir(parents=True, exist_ok=True)
+        diff.save(diff_out)
+        return False, rms, diff_out
+    return True, rms, None
diff --git a/tests/helpers/utils/canvas.py b/tests/helpers/utils/canvas.py
new file mode 100644
index 00000000..90f87020
--- /dev/null
+++ b/tests/helpers/utils/canvas.py
@@ -0,0 +1,120 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+"""
+Playwright canvas helpers for e2e and smoke tests.
+
+Provides reusable functions for:
+  - Navigating to the Visor viewer and waiting for network idle
+  - Waiting for a WebGL canvas to become alive (non-zero drawing buffer)
+  - Asserting that the canvas remains alive at labeled checkpoints
+
+"""
+
+from __future__ import annotations
+
+from playwright.sync_api import Page
+
+# Default canvas CSS selector โ€” matches the VTK WASM canvas used by Visor
+DEFAULT_CANVAS_SELECTOR = "canvas.vtk-wasm-1"
+
+
+def goto_and_wait(page: Page, url: str, *, wait_until: str = "networkidle") -> None:
+    """Navigate to *url* and wait for the page to settle.
+
+    Parameters
+    ----------
+    page : Page
+        Playwright page instance.
+    url : str
+        Target URL (e.g. ``http://localhost:8081/index.html``).
+    wait_until : str
+        Playwright wait strategy (default ``"networkidle"``).
+    """
+    page.set_viewport_size({"width": 1280, "height": 800})
+    page.goto(url, wait_until=wait_until)
+
+
+def wait_for_canvas(
+    page: Page,
+    *,
+    selector: str = DEFAULT_CANVAS_SELECTOR,
+    attached_timeout: int = 300_000,
+    visible_timeout: int = 15_000,
+):
+    """Wait until the canvas element is attached and visible.
+
+    Returns the Playwright Locator for the canvas.
+    """
+    canvas = page.locator(selector)
+    canvas.wait_for(state="attached", timeout=attached_timeout)
+    canvas.wait_for(state="visible", timeout=visible_timeout)
+    return canvas
+
+
+_WEBGL_ALIVE_JS = """
+() => {
+  const c = document.querySelector('canvas.vtk-wasm-1');
+  if (!c) return false;
+  const gl = c.getContext('webgl2') || c.getContext('webgl');
+  if (!gl) return false;
+  return gl.drawingBufferWidth > 0 && gl.drawingBufferHeight > 0;
+}
+"""
+
+
+def wait_for_webgl(page: Page, *, timeout_ms: int = 10_000) -> None:
+    """Block until the WebGL context on the canvas has a non-zero drawing buffer.
+
+    Raises TimeoutError if the condition isn't met within *timeout_ms*.
+    """
+    page.wait_for_function(_WEBGL_ALIVE_JS, timeout=timeout_ms)
+
+
+def assert_canvas_alive(page: Page, label: str, *, timeout_ms: int = 5_000) -> None:
+    """Assert the canvas is present, visible, and has a valid WebGL context.
+
+    Parameters
+    ----------
+    page : Page
+        Playwright page instance.
+    label : str
+        Human-readable checkpoint name used in error messages.
+    timeout_ms : int
+        Maximum wait time in milliseconds.
+
+    Raises
+    ------
+    AssertionError
+        If the canvas/WebGL context is not alive within the timeout.
+    """
+    try:
+        wait_for_webgl(page, timeout_ms=timeout_ms)
+    except Exception as exc:
+        # page.wait_for_function raises its own TimeoutError on failure, so this
+        # is the only path that ever produces the labeled message below.
+        raise AssertionError(f"[{label}] Canvas/WebGL not alive") from exc
+
+
+def wait_for_canvas_alive(page: Page, label: str = "", *, timeout_ms: int = 15_000):
+    """Wait until the canvas is visible and WebGL is alive; return the canvas locator.
+
+    Unlike `assert_canvas_alive`, this also waits for the canvas to become
+    visible first, so it's suited to initial page loads rather than post-interaction checks.
+    """
+    step = f" [{label}]" if label else ""
+    try:
+        canvas = wait_for_canvas(page, visible_timeout=timeout_ms)
+    except Exception as exc:
+        raise exc.__class__(f"Canvas not visible{step}") from exc
+
+    try:
+        wait_for_webgl(page, timeout_ms=timeout_ms)
+    except Exception as exc:
+        raise exc.__class__(f"Canvas WebGL not ready{step}") from exc
+
+    return canvas
+
+
+def settle(page: Page, ms: int = 200) -> None:
+    """Small pause to allow rendering to settle after an interaction."""
+    page.wait_for_timeout(ms)
diff --git a/tests/helpers/utils/dash_server.py b/tests/helpers/utils/dash_server.py
new file mode 100644
index 00000000..32b878d7
--- /dev/null
+++ b/tests/helpers/utils/dash_server.py
@@ -0,0 +1,66 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+from dash import Dash, Input, Output, callback, html
+from visordash import Visordash, init_endpoints
+
+
+def run_dash_server(visor_host: str, visor_port: int, dash_port: int, **visordash_kwargs):
+    """
+    Function executed in a separate process to run the Dash app.
+
+    Parameters
+    ----------
+    visor_host : str
+        Hostname of the running Visor instance.
+    visor_port : int
+        Port of the running Visor instance.
+    dash_port : int
+        Port on which the Dash server will listen.
+    **visordash_kwargs
+        Optional overrides for Visordash props (e.g. ``darkMode=True``,
+        ``aspectRatio=1.0``, ``pixelDensity=750``).
+    """
+    app = Dash(__name__)
+
+    # Register Visor endpoints needed by the component
+    init_endpoints(app)
+
+    # Build props: defaults first, then caller overrides
+    td_props = {
+        "id": "input",
+        "host": visor_host,
+        "port": visor_port,
+        "pixelDensity": 1500,
+        "aspectRatio": 2.2,
+        "darkMode": False,
+    }
+    td_props.update(visordash_kwargs)
+
+    # Visordash component wired to running Visor instance
+    td = Visordash(**td_props)
+
+    app.layout = html.Div([
+        td,
+        html.Button("Get Parts Snapshot", id="snapshot-btn"),
+        html.Pre(id='output')
+    ])
+
+    @callback(
+        Output('input', 'fireSnapshot'),
+        Input('snapshot-btn', 'n_clicks'),
+        prevent_initial_call=True
+    )
+    def trigger_snapshot(n_clicks):
+        return True
+
+    @callback(
+        Output('output', 'children'),
+        Input('input', 'parts'),
+        prevent_initial_call=True
+    )
+    def display_parts(parts):
+        import json
+        return json.dumps(parts, indent=2)
+
+    # Important: use_reloader=False to avoid double processes
+    app.run(host='0.0.0.0', port=dash_port, debug=False, use_reloader=False)
\ No newline at end of file
diff --git a/tests/helpers/utils/mesh_creator.py b/tests/helpers/utils/mesh_creator.py
new file mode 100644
index 00000000..01e39338
--- /dev/null
+++ b/tests/helpers/utils/mesh_creator.py
@@ -0,0 +1,97 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+from typing import List
+
+import numpy as np
+from vtk import vtkFloatArray, vtkSphereSource
+
+
+class MeshCreator:
+    """
+    Helper class to create a vtkPolyData mesh (sphere) and add variables to it.
+
+    Parameters
+    ----------
+    scale : float
+        Scale (radius) of the sphere.
+    xoffset : float
+        Offset of the sphere center along the x-axis.
+
+    Attributes
+    ----------
+    polydata : vtkPolyData
+        The generated sphere mesh.
+    num_points : int
+        Number of points in the mesh.
+
+    """
+    def __init__(self, scale=1.0, xoffset=0.0):
+        self.scale = scale
+        self.xoffset = xoffset
+        self.polydata = self._generate_polydata()
+
+    @property
+    def num_points(self):
+        return self.polydata.GetNumberOfPoints()
+
+    def add_variable(self, name, num_components, values):
+        vals = np.asarray(values, dtype=np.float32)
+
+        # Enforce correct shape
+        if vals.shape != (self.num_points, num_components):
+            raise ValueError(
+                f"Expected shape ({self.num_points}, {num_components}), "
+                f"got {vals.shape}"
+            )
+
+        arr = vtkFloatArray()
+        arr.SetName(name)
+        arr.SetNumberOfComponents(num_components)
+        arr.SetNumberOfTuples(self.num_points)
+
+        for i in range(self.num_points):
+            if num_components == 1:
+                arr.SetValue(i, float(vals[i, 0]))
+            else:
+                row = vals[i, :].tolist()
+                arr.SetTuple(i, row)
+        self.polydata.GetPointData().AddArray(arr)
+        self.polydata.GetPointData().SetActiveScalars(name)
+
+    def add_constant_variable(self, name: str, num_components: int, constant_values: List[float]):
+        values = self.get_constant_values(num_components, constant_values)
+        self.add_variable(name, num_components, values)
+
+    def add_random_variable(self, name: str, num_components: int, scale_factor=1.0):
+        values = self.get_random_values(num_components, scale_factor=scale_factor)
+        self.add_variable(name, num_components, values)
+
+    def get_constant_values(self, num_components: int, constant_values: List[float]) -> np.ndarray:
+        """
+        Create an array with a different constant value per component.
+        The values list is used to set the constant value across all points for the corresponding component.
+        """
+        if len(constant_values) != num_components:
+            raise ValueError("constant_values list length needs to equal num_components")
+
+        # Numpy array of shape (self.num_points, num_components)
+        arr = np.empty((self.num_points, num_components), dtype=np.float32)
+        for i in range(0, num_components):
+            arr[:, i] = float(constant_values[i])
+        return arr
+
+    def get_random_values(self, num_components=1, scale_factor=1.0) -> np.ndarray:
+        """
+        Create an array with random variables for each component, from 0 to scale_factor.
+        """
+        rng = np.random.default_rng() # Use a seed for reproducibility
+        return (scale_factor * rng.random((self.num_points, num_components), dtype=np.float32))
+
+    def _generate_polydata(self):
+        source = vtkSphereSource()
+        source.SetRadius(self.scale)
+        source.SetThetaResolution(32)
+        source.SetPhiResolution(32)
+        source.SetCenter(self.xoffset, 0, 0)
+        source.Update()
+        return source.GetOutput()
\ No newline at end of file
diff --git a/tests/helpers/utils/ports.py b/tests/helpers/utils/ports.py
new file mode 100644
index 00000000..6f0dccdd
--- /dev/null
+++ b/tests/helpers/utils/ports.py
@@ -0,0 +1,46 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+from __future__ import annotations
+
+import socket
+from contextlib import contextmanager
+from typing import Iterator, Tuple
+
+
+def _exclusive_sock() -> socket.socket:
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    # Prefer exclusive use on Windows to avoid TIME_WAIT and reuse pitfalls
+    if hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
+        s.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+    # Do NOT set SO_REUSEADDR for a guard/probe since apparently it can mask conflicts on Unix.
+    return s
+
+def reserve_free_port(host: str = "127.0.0.1") -> Tuple[str, int, socket.socket]:
+    """
+    Bind a guard socket to an ephemeral port chosen by the OS and return (host, port, guard_socket).
+    - Keep the guard socket bound to the port until the server is ready to request its use.
+    """
+    s = _exclusive_sock()
+    # Ask OS for a free ephemeral port
+    s.bind((host, 0))
+    # Listen so the port is truly reserved for TCP
+    s.listen(1)
+    addr, port = s.getsockname()
+
+    return host, port, s
+
+@contextmanager
+def reserved_port(host: str = "127.0.0.1") -> Iterator[Tuple[str, int, socket.socket]]:
+    """
+    Context manager that reserves a free port and yields (host, port, guard_socket).
+    guard_socket must stay open until the server is listening, then close it.
+    """
+    h, p, guard = reserve_free_port(host)
+    try:
+        yield h, p, guard
+    finally:
+        try:
+            guard.close()
+        except Exception:
+             pass
+
diff --git a/tests/helpers/utils/process.py b/tests/helpers/utils/process.py
new file mode 100644
index 00000000..4ac9162f
--- /dev/null
+++ b/tests/helpers/utils/process.py
@@ -0,0 +1,18 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+from __future__ import annotations
+
+import os
+import signal
+import subprocess
+
+
+def kill_proc_tree(pid: int) -> None:
+    """Kill a process and all its children, cross-platform."""
+    if os.name == "nt":
+        subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], capture_output=True)
+    else:
+        try:
+            os.killpg(os.getpgid(pid), signal.SIGKILL)
+        except ProcessLookupError:
+            pass
diff --git a/tests/helpers/utils/ui_selectors.py b/tests/helpers/utils/ui_selectors.py
new file mode 100644
index 00000000..f776f976
--- /dev/null
+++ b/tests/helpers/utils/ui_selectors.py
@@ -0,0 +1,176 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+"""
+UI selector strategy for Visor frontend Playwright tests.
+
+1. Structural selectors (CSS class + :has() for panel identity)
+2. Role/input type selectors
+3. page.evaluate() for complex DOM queries
+
+Since the frontend uses fully randomized numeric IDs, this module provides stable
+locator factories based on classes and element structure that tests should use
+instead of raw selectors.
+"""
+
+from __future__ import annotations
+
+from playwright.sync_api import Locator, Page
+
+# ---------------------------------------------------------------------------
+# 1. Structural selectors (panel containers)
+# ---------------------------------------------------------------------------
+
+def tree_panel(page: Page) -> Locator:
+    """Top-left panel containing the scene tree."""
+    return page.locator('.theme-panel-1:has(.visor-tree-view)')
+
+
+def properties_panel(page: Page) -> Locator:
+    """Top-right panel containing properties/legend."""
+    return page.locator('.theme-panel-1:has(input[type="color"])')
+
+
+def canvas_container(page: Page) -> Locator:
+    """VTK canvas container."""
+    return page.locator('.visor-viewer-background')
+
+
+def bottom_panel(page: Page) -> Locator:
+    """Bottom-middle panel (table/info)."""
+    return page.locator('[id^="tableElem-"]').locator('..')
+
+
+# ---------------------------------------------------------------------------
+# 2. Structural selectors (CSS class + context)
+# ---------------------------------------------------------------------------
+
+def tree_root(page: Page) -> Locator:
+    """Root tree view container (.visor-tree-view)."""
+    return tree_panel(page).locator(".visor-tree-view")
+
+
+def tree_node_by_name(page: Page, name: str) -> Locator:
+    """Locate a tree node row by its text content."""
+    return tree_root(page).locator(f"table:has-text('{name}')")
+
+
+def selected_tree_node(page: Page) -> Locator:
+    """Currently selected tree row (highlighted)."""
+    return tree_root(page).locator("table.theme-selected-background")
+
+
+# ---------------------------------------------------------------------------
+# 3. Role/input type selectors within panels
+# ---------------------------------------------------------------------------
+
+def opacity_slider(page: Page) -> Locator:
+    """Opacity range input in the properties panel."""
+    return properties_panel(page).locator('input[type="range"]')
+
+
+def opacity_text_input(page: Page) -> Locator:
+    """Opacity text input in the properties panel."""
+    return properties_panel(page).locator('input[type="text"]').first
+
+
+def color_picker(page: Page) -> Locator:
+    """Native color picker input in the properties panel."""
+    return properties_panel(page).locator('input[type="color"]')
+
+
+# ---------------------------------------------------------------------------
+# 4. page.evaluate() helpers
+# ---------------------------------------------------------------------------
+
+def get_tree_node_names(page: Page) -> list[str]:
+    """Extract all tree node names via JS."""
+    return page.evaluate("""
+        () => {
+            const treeView = document.querySelector('.visor-tree-view');
+            if (!treeView) return [];
+            const tables = treeView.querySelectorAll('table');
+            return Array.from(tables).map(t => t.textContent.trim()).filter(Boolean);
+        }
+    """)
+
+
+def get_opacity_value(page: Page) -> float:
+    """Get current opacity value (0-1 scale) from the slider."""
+    val = page.evaluate("""
+        () => {
+            const panel = document.querySelector('.theme-panel-1:has(input[type="color"])');
+            if (!panel) return null;
+            const range = panel.querySelector('input[type="range"]');
+            return range ? parseInt(range.value) / 100.0 : null;
+        }
+    """)
+    return float(val) if val is not None else 1.0
+
+
+def get_color_value(page: Page) -> str:
+    """Get current color picker hex value."""
+    return page.evaluate("""
+        () => {
+            const panel = document.querySelector('.theme-panel-1:has(input[type="color"])');
+            if (!panel) return '#000000';
+            const input = panel.querySelector('input[type="color"]');
+            return input ? input.value : '#000000';
+        }
+    """)
+
+
+def get_visor_viewer_background(page: Page) -> str | None:
+    """Extract --visor-viewer-background CSS variable value from #__visorThemeStyle."""
+    return page.evaluate("""
+        () => {
+            const style = document.getElementById('__visorThemeStyle');
+            if (!style) return null;
+            const match = style.textContent.match(/--visor-viewer-background:\\s*([^;]+)/);
+            return match ? match[1].trim() : null;
+        }
+    """)
+
+
+def get_container_aspect_ratio(page: Page) -> str | None:
+    """Extract aspect-ratio style value from the Visor App root container.
+
+    """
+    return page.evaluate("""
+        () => {
+            const canvas = document.querySelector('.visor-viewer-background');
+            if (!canvas) return null;
+            const scaffold = canvas.parentElement;
+            if (!scaffold) return null;
+            const appRoot = scaffold.parentElement;
+            return appRoot ? (appRoot.style.aspectRatio || null) : null;
+        }
+    """)
+
+
+def _parse_scale_from_transform(raw: str) -> float | None:
+    """Parse the scale value from an inline CSS transform string.
+
+    Visor's setScale() always writes ``scale(x, y)`` to inline style.
+    """
+    import re
+
+    if not raw or raw == "none":
+        return None
+
+    m = re.search(r"scale(?:3d)?\(\s*([-+]?\d*\.?\d+)", raw, re.I)
+    return float(m[1]) if m else None
+
+
+def get_ui_panel_scale(page: Page) -> float | None:
+    """Extract UI transform scale from the top-left panel's scaled wrapper.
+
+    """
+    transform = page.evaluate("""
+        () => {
+            const panel = document.querySelector('.theme-panel-1:has(.visor-tree-view)');
+            if (!panel || !panel.parentElement) return null;
+            const wrapper = panel.parentElement;
+            return wrapper.style.transform || getComputedStyle(wrapper).transform || null;
+        }
+    """)
+    return _parse_scale_from_transform(transform) if transform else None
diff --git a/tests/helpers/utils/visor_api_tool.py b/tests/helpers/utils/visor_api_tool.py
new file mode 100644
index 00000000..fa0ff82d
--- /dev/null
+++ b/tests/helpers/utils/visor_api_tool.py
@@ -0,0 +1,194 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+"""Interact with a running SAF-launched Visor instance's FastAPI management server.
+
+This is done so e2e tests can discover the Visor management API and drive it (e.g. save_state)
+without going through the SAF Dash UI.
+
+The API host:port is assigned dynamically by PIM per project and isn't persisted
+by saf-visor-poc, so you must supply it explicitly (--api-host/--api-port) or point
+--log-file at a saf run log whose stdout was redirected to a file (e.g. via
+
+
+Subcommands
+-----------
+list-datasets   List loaded datasets, then the variables (grouped by part) of the
+                sole loaded dataset. If more than one dataset is loaded, a warning
+                is printed and only the first dataset's variables are shown.
+save-state      Save the visualizer's current state to a directory and print where
+                it was saved. The directory path is resolved on the machine Visor
+                is running on, not necessarily the machine running this script.
+
+Examples
+--------
+python visor_api_tool.py --api-host 127.0.0.1 --api-port 60537 list-datasets
+python visor_api_tool.py --log-file /tmp/saf_run.log save-state /tmp/visor_state
+"""
+
+import argparse
+import json
+import re
+import sys
+
+import requests
+
+API_HOST_PORT_RE = re.compile(r"Visor API host:\s*(.+?),\s*port:\s*(\d+)", re.DOTALL)
+
+
+def _read_log_text(log_file: str) -> str:
+    """Read a log file, auto-detecting its encoding.
+    """
+    with open(log_file, "rb") as f:
+        raw = f.read()
+    if raw[:2] in (b"\xff\xfe", b"\xfe\xff"):
+        return raw.decode("utf-16")
+    if raw[:3] == b"\xef\xbb\xbf":
+        return raw.decode("utf-8-sig")
+    return raw.decode("utf-8", errors="replace")
+
+
+def discover_from_log(log_file: str) -> tuple[str, int]:
+    """Extract the Visor management API host/port from a saf run log file."""
+    content = _read_log_text(log_file)
+
+    match = API_HOST_PORT_RE.search(content)
+    if not match:
+        raise RuntimeError(
+            f"Could not find a 'Visor API host: , port: ' line in {log_file}. "
+            "Make sure the SAF project has launched Visor at least once, and that "
+            "safs stdout was redirected to the log file."
+        )
+    host = match.group(1).strip()
+    return host, int(match.group(2))
+
+
+def _request(method: str, api_host: str, api_port: int, path: str, **kwargs) -> dict:
+    """Call the Visor management API and return the parsed JSON body."""
+    base = f"http://{api_host}:{api_port}"
+    resp = requests.request(method, f"{base}{path}", **kwargs)
+    if resp.status_code == 503:
+        raise RuntimeError(
+            "Visor responded 503: no active visualizer instance. "
+            "The SAF project must have called /initialize + /start (i.e. Visor "
+            "must have been opened at least once) before this operation can be performed."
+        )
+    resp.raise_for_status()
+    if "application/json" not in resp.headers.get("Content-Type", ""):
+        raise RuntimeError(
+            f"{api_host}:{api_port} did not return JSON (Content-Type: "
+            f"{resp.headers.get('Content-Type')!r}). This is likely NOT the Visor management "
+            "API - check you didn't pass SAF's Solution UI or Solution API port by mistake."
+        )
+    return resp.json()
+
+
+def list_datasets(api_host: str, api_port: int) -> dict:
+    """Call the Visor management API and return the loaded datasets."""
+    return _request("GET", api_host, api_port, "/list_datasets", timeout=30).get("datasets", {})
+
+
+def list_variables(api_host: str, api_port: int, dataset_id: int) -> list:
+    """Call the Visor management API and return the variables for a dataset, grouped by part."""
+    return _request("GET", api_host, api_port, f"/{dataset_id}/list_variables", timeout=30).get("parts", [])
+
+
+def save_state(api_host: str, api_port: int, state_dir: str) -> str:
+    """Call the Visor management API to save the current state; return the server's success message."""
+    body = _request("POST", api_host, api_port, "/save_state", json={"state_dir": state_dir}, timeout=60)
+    return body.get("success", "")
+
+
+def resolve_api_host_port(args: argparse.Namespace, parser: argparse.ArgumentParser) -> tuple[str, int]:
+    """Resolve --api-host/--api-port either directly or via --log-file discovery."""
+    if args.log_file:
+        try:
+            return discover_from_log(args.log_file)
+        except RuntimeError as exc:
+            print(str(exc), file=sys.stderr)
+            sys.exit(1)
+    if args.api_host and args.api_port:
+        return args.api_host, args.api_port
+    parser.error("Provide either --log-file, or both --api-host and --api-port.")
+
+
+def run_list_datasets(api_host: str, api_port: int) -> None:
+    try:
+        datasets = list_datasets(api_host, api_port)
+    except requests.ConnectionError:
+        print(f"Could not connect to {api_host}:{api_port}. Is the Visor API server running?", file=sys.stderr)
+        sys.exit(1)
+    except RuntimeError as exc:
+        print(str(exc), file=sys.stderr)
+        sys.exit(1)
+
+    if not datasets:
+        print("No datasets are currently loaded.")
+        return
+
+    print(json.dumps(datasets, indent=2))
+
+    if len(datasets) > 1:
+        print(
+            f"\nWarning: {len(datasets)} datasets are loaded, showing variables for the first one only.",
+            file=sys.stderr,
+        )
+    dataset_id = next(iter(datasets))
+
+    try:
+        parts = list_variables(api_host, api_port, int(dataset_id))
+    except RuntimeError as exc:
+        print(str(exc), file=sys.stderr)
+        sys.exit(1)
+
+    print(f"\nVariables for dataset {dataset_id}:")
+    print(json.dumps(parts, indent=2))
+
+
+def run_save_state(api_host: str, api_port: int, state_dir: str) -> None:
+    try:
+        message = save_state(api_host, api_port, state_dir)
+    except requests.ConnectionError:
+        print(f"Could not connect to {api_host}:{api_port}. Is the Visor API server running?", file=sys.stderr)
+        sys.exit(1)
+    except RuntimeError as exc:
+        print(str(exc), file=sys.stderr)
+        sys.exit(1)
+
+    print(message)
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument("--api-host", help="Host of the Visor management API server.")
+    parser.add_argument("--api-port", type=int, help="Port of the Visor management API server.")
+    parser.add_argument("--log-file", help="Path to a saf run log to auto-discover --api-host/--api-port from.")
+
+    subparsers = parser.add_subparsers(dest="command", required=True)
+
+    subparsers.add_parser(
+        "list-datasets",
+        help="List loaded datasets and the variables of the sole loaded dataset.",
+    )
+
+    save_state_parser = subparsers.add_parser(
+        "save-state",
+        help="Save the visualizer's current state to a directory.",
+    )
+    save_state_parser.add_argument(
+        "state_dir",
+        help="Directory to save the state to (resolved on the Visor server's machine).",
+    )
+
+    args = parser.parse_args()
+    api_host, api_port = resolve_api_host_port(args, parser)
+
+    print(f"Using Visor management API at {api_host}:{api_port}", file=sys.stderr)
+
+    if args.command == "list-datasets":
+        run_list_datasets(api_host, api_port)
+    elif args.command == "save-state":
+        run_save_state(api_host, api_port, args.state_dir)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/helpers/visual.py b/tests/helpers/visual.py
new file mode 100644
index 00000000..9898b553
--- /dev/null
+++ b/tests/helpers/visual.py
@@ -0,0 +1,164 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+
+from __future__ import annotations
+
+import shutil
+import time
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Optional
+
+import pytest
+
+
+@dataclass(frozen=True)
+class VisualCheckResult:
+    """Result of a visual comparison."""
+    name: str
+    passed: bool
+    rms: float
+    current_path: Path
+    baseline_path: Path
+    diff_path: Optional[Path]
+    message: str = ""
+
+
+@dataclass(frozen=True)
+class TestResult:
+    """Aggregates results from multiple visual checks."""
+    # make sure any amount of VisualCheckResults are allowed in the checks tuple
+    checks: tuple[VisualCheckResult, ...]
+
+    @property
+    def passed(self) -> bool:
+        """Return True only if all checks have passed."""
+        for check in self.checks:
+            if not check.passed:
+                return False
+        return True
+
+    @property
+    def failed_checks(self) -> list[VisualCheckResult]:
+        """Return all checks that did not pass."""
+        failed = []
+        for check in self.checks:
+            if not check.passed:
+                failed.append(check)
+        return failed
+
+    def summary(self) -> str:
+        """Return a formatted summary of all visual checks for logging."""
+
+        lines = []
+
+        for check in self.checks:
+            status = "PASS" if check.passed else "FAIL"
+            parts = [f"[{status}] {check.name}"]
+
+            # RMS value
+            if check.rms is not None:
+                parts.append(f"RMS={check.rms:.3f}")
+
+            # Additional message
+            if check.message:
+                parts.append(f"- {check.message}")
+
+            # Paths for failed checks
+            if not check.passed:
+                if check.current_path:
+                    parts.append(f"current={check.current_path}")
+                if check.baseline_path:
+                    parts.append(f"baseline={check.baseline_path}")
+                if check.diff_path:
+                    parts.append(f"diff={check.diff_path}")
+
+            line = " | ".join(parts)
+            lines.append(line)
+
+        return "\n".join(lines)
+
+def make_test_result(*checks: VisualCheckResult) -> TestResult:
+
+    """
+    Convenience factory to build a TestResult from multiple CheckResults.
+    """
+    return TestResult(checks=tuple(checks))
+
+
+def verify_canvas_against_baseline(
+    *,
+    canvas_locator,                 # Playwright locator: e.g., page.locator("canvas.vtk-wasm-1")
+    baseline_dir: Path,             # pytest fixture-provided baseline dir (Path) for image canonicals
+    pixel_threshold: float,         # pytest fixture-provided threshold for image comparison
+    request,                        # pytest's request fixture (to read --update-baseline)
+    compare_images,                 # perform comparison
+    test_id,                        # ID used in filenames (customize per test)
+    test_suite: str = "",           # Name of the test suite (e.g., "regressions") for organizing artifacts
+    message: str = ""
+
+) -> VisualCheckResult:
+
+    """
+    Take a screenshot of a canvas element, compare it to a baseline image, and assert
+
+    Behavior:
+      - Saves a timestamped current screenshot under tests/artifacts/{test_suite}
+      - If `--update-baseline` is set or baseline doesn't exist, copies current -> baseline and skips the test.
+      - Otherwise, compares current vs baseline with `compare_images` and asserts on failure.
+
+    Args:
+        canvas_locator: Playwright locator already pointing to the canvas to capture.
+        baseline_dir: Directory where baseline images are stored (e.g., tests/reference_images)
+        pixel_threshold: RMS threshold used by compare_images.
+        request: pytest's built-in request fixture to read CLI options (e.g., --update-baseline)
+        compare_images: A callable function that performs image comparison:
+            (current_path: Path, baseline_path: Path, diff_path: Path, threshold: float)
+            -> tuple[bool, float, Optional[Path]]
+        test_id: Stem used in filenames to distinguish different tests
+        screenshots_root: Root folder for storing current and diff artifacts
+        message: Custom message to add to the TestResult summary for additional context
+
+    Returns:
+        VisualCheckResult with pass/fail, RMS value, and artifact paths
+
+    Raises:
+        pytest.skip: If baseline is updated/initialized during this run
+        AssertionError: If comparison fails the threshold.
+    """
+    # Ensure output directory exists
+    screenshots_root = Path("tests", "artifacts", test_suite, "screenshots")
+    screenshots_root.mkdir(parents=True, exist_ok=True)
+
+    # Timestamped filenames
+    stamp = time.strftime("%Y%m%d-%H%M%S")
+    current_shot = screenshots_root / f"{test_id}_current_{stamp}.png"
+    diff_path = screenshots_root / f"{test_id}_diff_{stamp}.png"
+    baseline_shot = baseline_dir / f"{test_id}_baseline.png"
+
+    # Capture screenshot of just the canvas element
+    canvas_locator.screenshot(path=str(current_shot))
+
+    # Baseline update behavior
+    update_baseline = bool(request.config.getoption("--update-baseline"))
+    if update_baseline or not baseline_shot.exists():
+        baseline_shot.parent.mkdir(parents=True, exist_ok=True)
+        shutil.copy2(current_shot, baseline_shot)
+        pytest.skip(
+            f"Baseline {'initialized' if not baseline_shot.exists() else 'updated'} "
+            f"for {test_id}; re-run without --update-baseline to validate."
+        )
+
+    # Compare and assert
+    passed, rms, saved = compare_images(
+        current_shot, baseline_shot, diff_path, threshold=pixel_threshold
+    )
+
+    return VisualCheckResult(
+        passed=passed,
+        rms=rms,
+        current_path=current_shot,
+        baseline_path=baseline_shot,
+        diff_path=saved,
+        name = test_id,
+        message= message
+    )
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/integration/test_async_runner.py b/tests/integration/test_async_runner.py
new file mode 100644
index 00000000..c35950d5
--- /dev/null
+++ b/tests/integration/test_async_runner.py
@@ -0,0 +1,96 @@
+import os
+from typing import Coroutine
+
+import pytest
+
+from ansys.visor.viewer import Visor
+
+input_file = os.path.join(os.path.dirname(__file__), "..", "files", "plate.vtp")
+
+def test_start_stop():
+    """Test start and stop."""
+    vis = Visor(url="http://localhost:8081")
+    vis.start(input=input_file)
+    assert vis.running is True
+    vis.stop()
+    assert vis.running is False
+
+
+@pytest.mark.asyncio
+async def test_start_async_stop_async():
+    """Test start_async and stop_async."""
+    vis = Visor(url="http://localhost:8081")
+    dataset_id, coro = await vis._start_async(input=input_file)
+    assert vis.running is True
+    assert isinstance(coro, Coroutine)
+    await vis._stop_async()
+    assert vis.running is False
+
+
+@pytest.mark.asyncio
+async def test_start_async_stop():
+    """Test start_async and stop."""
+    vis = Visor(url="http://localhost:8081")
+    dataset_id, coro = await vis._start_async(input=input_file)
+    assert vis.running is True
+    assert isinstance(coro, Coroutine)
+    vis.stop()
+    assert vis.running is False
+
+
+@pytest.mark.asyncio
+async def test_start_stop_async():
+    """Test start and stop_async."""
+    vis = Visor(url="http://localhost:8081")
+    vis.start(input=input_file)
+    assert vis.running is True
+    await vis._stop_async()
+    assert vis.running is False
+
+
+def test_double_start():
+    """Test double start."""
+    vis = Visor(url="http://localhost:8081")
+    vis.start(input=input_file)
+    with pytest.raises(BaseException):
+        vis.start(input=input_file)
+    vis.stop()
+
+
+@pytest.mark.asyncio
+async def test_double_start_async():
+    """Test double start_async."""
+    vis = Visor(url="http://localhost:8081")
+    await vis._start_async(input=input_file)
+    with pytest.raises(BaseException):
+        await vis._start_async(input=input_file)
+    vis.stop()
+
+
+def test_update():
+    """Test update after start."""
+    vis = Visor(url="http://localhost:8081")
+    vis.start(input=input_file)
+    vis.update(input=input_file)
+    vis.stop()
+
+
+@pytest.mark.asyncio
+async def test_update_async():
+    """Test that update after start_async."""
+    vis = Visor(url="http://localhost:8081")
+    await vis._start_async(input=input_file)
+    vis.update(input=input_file)
+    await vis._stop_async()
+
+# async def main():
+#     test_start_stop()
+#     await test_start_async_stop_async()
+#     await test_start_async_stop()
+#     await test_start_stop_async()
+#     test_double_start()
+#     await test_double_start_async()
+#     await test_update_async()
+#
+#
+# asyncio.run(main())
diff --git a/tests/integration/test_http_api.py b/tests/integration/test_http_api.py
new file mode 100644
index 00000000..2a8be6ab
--- /dev/null
+++ b/tests/integration/test_http_api.py
@@ -0,0 +1,101 @@
+from os import path
+
+import pytest
+from httpx import ASGITransport, AsyncClient
+
+from ansys.visor.viewer.api.server import app
+
+input_file = path.join(path.dirname(__file__),"..", "files","plate.vtp")
+client_path = path.join(path.dirname(path.dirname(__file__)),'src','ansys','visor',"visor-client","dist")
+
+@pytest.mark.anyio
+async def test_info():
+    """Test the info API."""
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        response = await ac.get("/info")
+    assert response.status_code == 200
+
+
+@pytest.mark.anyio
+async def test_initialize():
+    """Test initialize API."""
+    host = "localhost"
+    port = 8088
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        response = await ac.post("/initialize", json={"host": "localhost", "port": 8088, "standalone": False})
+    assert response.status_code == 200
+    assert response.json() == {"message": f"Set active instance to http://{host}:{port}"}
+
+@pytest.mark.anyio
+async def test_root():
+    """Test API root."""
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        response = await ac.post("/initialize", json={"hostname": "localhost", "port": 8088, "standalone": True})
+        response = await ac.get("/")
+    assert response.status_code == 200
+    assert response.json() == {'urls': ['http://localhost:8081', 'http://localhost:8088']}
+
+@pytest.mark.anyio
+@pytest.mark.xfail(reason="Side effect results in no running loop error")
+async def test_start():
+    """Test start API."""
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        await ac.get("/")
+        response = await ac.post("/start", json={'file_path': input_file, 'metadata': { 'name': "model", 'unit': "m" }, 'timeout':3})
+        assert response.status_code == 200
+
+@pytest.mark.anyio
+async def test_update():
+    """Test update API."""
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        await ac.get("/")
+        file = path.join(path.dirname(__file__), "..", "files","plate.vtp")
+        response = await ac.post("/start", json={'timeout':4})
+        response = await ac.post("/update", json={'file_path': file, 'metadata': { 'name': "model", 'unit': "m"} })
+        assert response.status_code == 200
+
+@pytest.mark.anyio
+async def test_health_active():
+    """Test health API."""
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        await ac.get("/")
+        await ac.post("/start")
+        response = await ac.get("/health")
+        assert response.status_code == 200
+
+@pytest.mark.anyio
+@pytest.mark.xfail(reason="Known InvalidStateError in server on /stop_visualization")
+async def test_stop_visualization():
+    """Test stop_visualization API."""
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        await ac.post("/start")
+        response = await ac.post("/stop_visualization")
+        # Accept 200 or known 500 error due to InvalidStateError
+        if response.status_code == 500:
+            pytest.skip("Known InvalidStateError in server, skipping test.")
+        assert response.status_code == 200
+
+@pytest.mark.anyio
+async def test_health_inactive():
+    """Test that health is inactive."""
+    async with AsyncClient(
+        transport=ASGITransport(app=app), base_url="http://test"
+    ) as ac:
+        response = await ac.post("/initialize", json={"hostname": "localhost", "port": 8090, "standalone": False})
+        response = await ac.get("/health")
+        assert response.status_code == 200
+
diff --git a/tests/integration/test_input_data.py b/tests/integration/test_input_data.py
new file mode 100644
index 00000000..840433ab
--- /dev/null
+++ b/tests/integration/test_input_data.py
@@ -0,0 +1,444 @@
+import os
+
+import vtk
+from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet, vtkPolyData, vtkUnstructuredGrid
+
+from ansys.visor.viewer.vtk.io.file_to_dataset import file_to_dataset
+from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraph
+
+input_file_plate = os.path.join(os.path.dirname(__file__),"..", "files","plate.vtp")
+input_file_mesh = os.path.join(os.path.dirname(__file__),"..", "files","mesh.vtu")
+input_file_many_blocks = os.path.join(os.path.dirname(__file__), "..", "files", "many_blocks", "many_blocks.vtm")
+
+# vtkhdf equivalents
+input_file_plate_vtkhdf = os.path.join(os.path.dirname(__file__), "..", "files", "plate.vtkhdf")
+input_file_mesh_vtkhdf = os.path.join(os.path.dirname(__file__), "..", "files", "mesh.vtkhdf")
+input_file_multiblock_vtkhdf = os.path.join(os.path.dirname(__file__), "..", "files", "simple_multiblock.vtkhdf")
+
+
+def test_input_vtp_file_to_dataset():
+    """Test file_to_dataset with VTP input."""
+    dataset = file_to_dataset(input_file_plate)
+    assert dataset.IsA("vtkDataSet")
+
+
+def test_input_vtu_file_to_dataset():
+    """Test file_to_dataset with VTKU input."""
+    dataset = file_to_dataset(input_file_mesh)
+    assert dataset.IsA("vtkDataSet")
+
+
+def test_input_vtm_file_to_dataset():
+    """Test file_to_dataset with VTM input."""
+    dataset = file_to_dataset(input_file_many_blocks)
+    assert dataset.IsA("vtkCompositeDataSet")
+
+
+def test_input_data_read_multi_block_input():
+    """Test multiblock Python input is read correctly."""
+    dataset = file_to_dataset(input_file_many_blocks)
+    # test root node
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+    descendant_nodes_or_self = root_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = root_node.get_descendant_nodes(include_self=False)
+    assert root_node.vtk_dataset_type == "root"
+    assert root_node.is_group_node
+    assert len(descendant_nodes) == 75
+    assert len(descendant_nodes_or_self) == 76
+    assert descendant_nodes_or_self[0] == root_node
+    assert len(root_node.children) == 1
+    # test file node
+    file_node = root_node.children[0]
+    assert file_node.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert file_node.is_group_node
+    assert file_node.bounds == root_node.bounds
+    descendant_nodes_or_self = file_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = file_node.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 74
+    assert len(descendant_nodes_or_self) == 75
+    assert descendant_nodes_or_self[0] == file_node
+    assert len(file_node.children) == 2
+    # test Group A
+    group_a = file_node.children[0]
+    assert group_a.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_a.is_group_node
+    descendant_nodes_or_self = group_a.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_a.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 65
+    assert len(descendant_nodes_or_self) == 66
+    assert descendant_nodes_or_self[0] == group_a
+    assert len(group_a.children) == 3    # test Group AA
+    group_aa = group_a.children[0]
+    assert group_aa.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_aa.is_group_node
+    descendant_nodes_or_self = group_aa.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_aa.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 20
+    assert len(descendant_nodes_or_self) == 21
+    assert descendant_nodes_or_self[0] == group_aa
+    assert len(group_aa.children) == 10
+    for node in group_aa.children:
+        if node.is_group_node:
+            continue
+        assert node.vtk_dataset_type == "vtkPolyData"
+        assert not node.is_group_node
+        descendant_nodes_or_self = node.get_descendant_nodes(include_self=True)
+        descendant_nodes = node.get_descendant_nodes(include_self=False)
+        assert len(descendant_nodes) == 0
+        assert len(descendant_nodes_or_self) == 1
+        assert descendant_nodes_or_self[0] == node
+    # test Group AAA
+    group_aaa = [node for node in group_aa.children if node.is_group_node][0]
+    assert group_aaa.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_aaa.is_group_node
+    descendant_nodes_or_self = group_aaa.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_aaa.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 10
+    assert len(descendant_nodes_or_self) == 11
+    assert descendant_nodes_or_self[0] == group_aaa
+    assert len(group_aaa.children) == 10
+    for node in group_aaa.children:
+        if node.is_group_node:
+            continue
+        assert node.vtk_dataset_type == "vtkPolyData"
+        assert not node.is_group_node
+        descendant_nodes_or_self = node.get_descendant_nodes(include_self=True)
+        descendant_nodes = node.get_descendant_nodes(include_self=False)
+        assert len(descendant_nodes) == 0
+        assert len(descendant_nodes_or_self) == 1
+        assert descendant_nodes_or_self[0] == node
+    # test Group AB
+    group_ab = group_a.children[1]
+    assert group_ab.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_ab.is_group_node
+    descendant_nodes_or_self = group_ab.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_ab.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 10
+    assert len(descendant_nodes_or_self) == 11
+    assert descendant_nodes_or_self[0] == group_ab
+    assert len(group_ab.children) == 10
+    for node in group_ab.children:
+        if node.is_group_node:
+            continue
+        assert node.vtk_dataset_type == "vtkPolyData"
+        assert not node.is_group_node
+        descendant_nodes_or_self = node.get_descendant_nodes(include_self=True)
+        descendant_nodes = node.get_descendant_nodes(include_self=False)
+        assert len(descendant_nodes) == 0
+        assert len(descendant_nodes_or_self) == 1
+        assert descendant_nodes_or_self[0] == node
+    # test Group AC
+    group_ac = group_a.children[2]
+    assert group_ac.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_ac.is_group_node
+    descendant_nodes_or_self = group_ac.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_ac.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 32
+    assert len(descendant_nodes_or_self) == 33
+    assert descendant_nodes_or_self[0] == group_ac
+    assert len(group_ac.children) == 1
+    # test Group ACA
+    group_aca = group_ac.children[0]
+    assert group_aca.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_aca.is_group_node
+    descendant_nodes_or_self = group_aca.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_aca.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 31
+    assert len(descendant_nodes_or_self) == 32
+    assert descendant_nodes_or_self[0] == group_aca
+    assert len(group_aca.children) == 20
+    for node in group_aca.children:
+        if node.is_group_node:
+            continue
+        assert node.vtk_dataset_type == "vtkPolyData"
+        assert not node.is_group_node
+        descendant_nodes_or_self = node.get_descendant_nodes(include_self=True)
+        descendant_nodes = node.get_descendant_nodes(include_self=False)
+        assert len(descendant_nodes) == 0
+        assert len(descendant_nodes_or_self) == 1
+        assert descendant_nodes_or_self[0] == node
+    # test Group ACAA
+    group_acaa = [node for node in group_aca.children if node.is_group_node][0]
+    assert group_acaa.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_acaa.is_group_node
+    descendant_nodes_or_self = group_acaa.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_acaa.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 11
+    assert len(descendant_nodes_or_self) == 12
+    assert descendant_nodes_or_self[0] == group_acaa
+    assert len(group_acaa.children) == 11
+    for node in group_acaa.children:
+        if node.is_group_node:
+            continue
+        assert node.vtk_dataset_type == "vtkPolyData"
+        assert not node.is_group_node
+        descendant_nodes_or_self = node.get_descendant_nodes(include_self=True)
+        descendant_nodes = node.get_descendant_nodes(include_self=False)
+        assert len(descendant_nodes) == 0
+        assert len(descendant_nodes_or_self) == 1
+        assert descendant_nodes_or_self[0] == node
+    # test Group B
+    group_b = file_node.children[1]
+    assert group_b.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert group_b.is_group_node
+    descendant_nodes_or_self = group_b.get_descendant_nodes(include_self=True)
+    descendant_nodes = group_b.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 7
+    assert len(descendant_nodes_or_self) == 8
+    assert descendant_nodes_or_self[0] == group_b
+    assert len(group_b.children) == 7
+    for node in group_b.children:
+        if node.is_group_node:
+            continue
+        assert node.vtk_dataset_type == "vtkPolyData"
+        assert not node.is_group_node
+        descendant_nodes_or_self = node.get_descendant_nodes(include_self=True)
+        descendant_nodes = node.get_descendant_nodes(include_self=False)
+        assert len(descendant_nodes) == 0
+        assert len(descendant_nodes_or_self) == 1
+        assert descendant_nodes_or_self[0] == node
+
+
+def test_input_data_read_poly_data_input():
+    """Test that PolyData Python input is read correctly."""
+    dataset = file_to_dataset(input_file_plate)
+
+    # test root node
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+    assert root_node.vtk_dataset_type == "root"
+    assert root_node.is_group_node
+    descendant_nodes_or_self = root_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = root_node.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 1
+    assert len(descendant_nodes_or_self) == 2
+    assert descendant_nodes_or_self[0] == root_node
+    assert len(root_node.children) == 1
+    # test file node
+    file_node = root_node.children[0]
+    assert file_node.vtk_dataset_type == "vtkPolyData"
+    assert not file_node.is_group_node
+    assert file_node.bounds == root_node.bounds
+    descendant_nodes_or_self = file_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = file_node.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 0
+    assert len(descendant_nodes_or_self) == 1
+    assert descendant_nodes_or_self[0] == file_node
+
+
+def test_input_data_read_unstructured_grid_input():
+    """Test that unstructured grid Python input is read correctly."""
+    dataset = file_to_dataset(input_file_mesh)
+
+    # test root node
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+    assert root_node.vtk_dataset_type == "root"
+    assert root_node.is_group_node
+    descendant_nodes_or_self = root_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = root_node.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 1
+    assert len(descendant_nodes_or_self) == 2
+    assert descendant_nodes_or_self[0] == root_node
+    assert len(root_node.children) == 1
+    # test file node
+    file_node = root_node.children[0]
+    assert file_node.vtk_dataset_type == "vtkUnstructuredGrid"
+    assert not file_node.is_group_node
+    assert file_node.bounds == file_node.bounds
+    descendant_nodes_or_self = file_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = file_node.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 0
+    assert len(descendant_nodes_or_self) == 1
+    assert descendant_nodes_or_self[0] == file_node
+
+def test_input_data_read_dataset_input():
+    """ Test that a random vtkPolyData Python input is read correctly."""
+    polydata_input = create_random_polydata_object()
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(polydata_input, None)
+    assert root_node.vtk_dataset_type == "root"
+    assert root_node.is_group_node
+    descendant_nodes_or_self = root_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = root_node.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 1
+    assert len(descendant_nodes_or_self) == 2
+    assert descendant_nodes_or_self[0] == root_node
+    assert len(root_node.children) == 1
+    # test file node
+    file_node = root_node.children[0]
+    assert file_node.vtk_dataset_type == "vtkPolyData"
+    assert not file_node.is_group_node
+    assert file_node.bounds == root_node.bounds
+    descendant_nodes_or_self = file_node.get_descendant_nodes(include_self=True)
+    descendant_nodes = file_node.get_descendant_nodes(include_self=False)
+    assert len(descendant_nodes) == 0
+    assert len(descendant_nodes_or_self) == 1
+    assert descendant_nodes_or_self[0] == file_node
+
+def create_random_polydata_object():
+        """Create a random vtkPolyData object for testing."""
+        points = vtk.vtkPoints()
+        for i in range(10):
+            points.InsertNextPoint(i, i, i)
+        polydata = vtk.vtkPolyData()
+        polydata.SetPoints(points)
+        return polydata
+
+def create_random_unstructured_grid_object():
+        """Create a random vtkUnstructuredGrid object for testing."""
+        points = vtk.vtkPoints()
+        for i in range(10):
+            points.InsertNextPoint(i, i, i)
+        unstructured_grid = vtk.vtkUnstructuredGrid()
+        unstructured_grid.SetPoints(points)
+        return unstructured_grid
+
+def create_random_multiblock_dataset_object():
+        """Create a random vtkMultiBlockDataSet object for testing."""
+        multiblock_dataset = vtk.vtkMultiBlockDataSet()
+        block1 = create_random_polydata_object()
+        block2 = create_random_unstructured_grid_object()
+        multiblock_dataset.SetBlock(0, block1)
+        multiblock_dataset.SetBlock(1, block2)
+        return multiblock_dataset
+
+# ================================================================== #
+# vtkhdf read support ??? file_to_dataset
+# ================================================================== #
+
+def test_input_vtkhdf_polydata_file_to_dataset():
+    """plate.vtkhdf must be read as a vtkPolyData (vtkDataSet)."""
+    dataset = file_to_dataset(input_file_plate_vtkhdf)
+    assert isinstance(dataset, vtkPolyData)
+    assert dataset.IsA("vtkDataSet")
+    assert dataset.GetNumberOfPoints() > 0
+    assert dataset.GetNumberOfCells() > 0
+
+
+def test_input_vtkhdf_unstructured_grid_file_to_dataset():
+    """mesh.vtkhdf must be read as a vtkUnstructuredGrid (vtkDataSet)."""
+    dataset = file_to_dataset(input_file_mesh_vtkhdf)
+    assert isinstance(dataset, vtkUnstructuredGrid)
+    assert dataset.IsA("vtkDataSet")
+    assert dataset.GetNumberOfPoints() > 0
+    assert dataset.GetNumberOfCells() > 0
+
+
+def test_input_vtkhdf_multiblock_file_to_dataset():
+    """simple_multiblock.vtkhdf must be read as a vtkMultiBlockDataSet (vtkCompositeDataSet)."""
+    dataset = file_to_dataset(input_file_multiblock_vtkhdf)
+    assert isinstance(dataset, vtkMultiBlockDataSet)
+    assert dataset.IsA("vtkCompositeDataSet")
+    assert dataset.GetNumberOfBlocks() > 0
+
+
+# ================================================================== #
+# vtkhdf read support ??? point/cell parity with legacy equivalents
+# ================================================================== #
+
+def test_vtkhdf_polydata_matches_vtp_point_and_cell_count():
+    """plate.vtkhdf and plate.vtp must contain the same geometry."""
+    vtp_dataset = file_to_dataset(input_file_plate)
+    vtkhdf_dataset = file_to_dataset(input_file_plate_vtkhdf)
+    assert vtkhdf_dataset.GetNumberOfPoints() == vtp_dataset.GetNumberOfPoints()
+    assert vtkhdf_dataset.GetNumberOfCells() == vtp_dataset.GetNumberOfCells()
+
+
+def test_vtkhdf_unstructured_grid_matches_vtu_point_and_cell_count():
+    """mesh.vtkhdf and mesh.vtu must contain the same geometry."""
+    vtu_dataset = file_to_dataset(input_file_mesh)
+    vtkhdf_dataset = file_to_dataset(input_file_mesh_vtkhdf)
+    assert vtkhdf_dataset.GetNumberOfPoints() == vtu_dataset.GetNumberOfPoints()
+    assert vtkhdf_dataset.GetNumberOfCells() == vtu_dataset.GetNumberOfCells()
+
+
+# ================================================================== #
+# vtkhdf read support ??? scene graph construction
+# ================================================================== #
+
+def test_vtkhdf_polydata_scene_graph_is_single_part_node():
+    """Loading plate.vtkhdf must produce a single vtkPolyData part node ??? same
+    shape as loading plate.vtp."""
+    dataset = file_to_dataset(input_file_plate_vtkhdf)
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+
+    assert root_node.vtk_dataset_type == "root"
+    assert root_node.is_group_node is True
+    assert len(root_node.children) == 1
+
+    file_node = root_node.children[0]
+    assert file_node.vtk_dataset_type == "vtkPolyData"
+    assert file_node.is_group_node is False
+    assert file_node.is_part_node is True
+    assert file_node.dataset is not None
+    assert file_node.dataset.IsA("vtkPolyData")
+    assert len(file_node.get_descendant_nodes(include_self=False)) == 0
+
+
+def test_vtkhdf_unstructured_grid_scene_graph_is_single_part_node():
+    """Loading mesh.vtkhdf must produce a single vtkUnstructuredGrid part node ???
+    same shape as loading mesh.vtu."""
+    dataset = file_to_dataset(input_file_mesh_vtkhdf)
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+
+    assert root_node.vtk_dataset_type == "root"
+    assert len(root_node.children) == 1
+
+    file_node = root_node.children[0]
+    assert file_node.vtk_dataset_type == "vtkUnstructuredGrid"
+    assert file_node.is_group_node is False
+    assert file_node.is_part_node is True
+    assert len(file_node.get_descendant_nodes(include_self=False)) == 0
+
+
+def test_vtkhdf_multiblock_scene_graph_is_group_node():
+    """Loading simple_multiblock.vtkhdf must produce a vtkMultiBlockDataSet group
+    node with at least one child actor ??? same structure as loading an equivalent .vtm."""
+    dataset = file_to_dataset(input_file_multiblock_vtkhdf)
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+
+    assert root_node.vtk_dataset_type == "root"
+    assert len(root_node.children) == 1
+
+    file_node = root_node.children[0]
+    assert file_node.vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert file_node.is_group_node is True
+
+    part_nodes = file_node.get_descendant_nodes(
+        include_self=False,
+        filter_func=lambda n: n.is_part_node
+    )
+    assert len(part_nodes) > 0
+    for part_node in part_nodes:
+        assert part_node.dataset is not None
+
+
+def test_vtkhdf_multiblock_block_names_are_preserved():
+    """Block names written into the vtkhdf multiblock file must survive the
+    read and appear in the scene graph node names."""
+    dataset = file_to_dataset(input_file_multiblock_vtkhdf)
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+
+    file_node = root_node.children[0]
+    name_to_id = file_node.get_descendant_node_name_to_id_map()
+    # simple_multiblock.vtkhdf was written with a block named "sphere"
+    assert "sphere" in name_to_id
+
+
+def test_vtkhdf_polydata_scene_graph_bounds_are_non_trivial():
+    """bounds reported by the scene graph node must be non-zero for real geometry."""
+    dataset = file_to_dataset(input_file_plate_vtkhdf)
+    root_node = VisorSceneGraph()
+    root_node.load_dataset(dataset, None)
+
+    file_node = root_node.children[0]
+    bounds = file_node.bounds
+    assert len(bounds) == 6
+    # At least one axis must span a non-zero range
+    assert any(bounds[i + 1] - bounds[i] > 0 for i in range(0, 6, 2))
diff --git a/tests/integration/test_python_api.py b/tests/integration/test_python_api.py
new file mode 100644
index 00000000..5678bb47
--- /dev/null
+++ b/tests/integration/test_python_api.py
@@ -0,0 +1,155 @@
+import asyncio
+from os import makedirs, path
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer import Visor
+from ansys.visor.viewer.app.visor_vtk import VisorVTK
+from ansys.visor.viewer.core.errors import InvalidFileError, InvalidFileFormatError, InvalidServerTimeoutError
+from ansys.visor.viewer.core.metadata import Metadata
+from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1
+
+plate_vtp_file = path.join(path.dirname(__file__), "..", "files", "plate.vtp")
+plate_scdocx_file = path.join(path.dirname(__file__), "..", "files", "plate.scdocx")
+mesh_vtu_file = path.join(path.dirname(__file__), "..", "files", "mesh.vtu")
+vtm_file = path.join(path.dirname(__file__), "..", "files", "many_blocks", "many_blocks.vtm")
+metadata = Metadata(name="test_model", unit="m")
+from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet
+
+
+def test_initialization():
+    """Test initialization."""
+    visor_instance = Visor()
+    assert visor_instance.standalone
+    assert visor_instance.host == "localhost"
+    assert visor_instance.port == 8081
+
+
+def test_initiazation_invalid_rendering_engine():
+    """Test initialization with invalid rendering engine."""
+    with pytest.raises(ValueError):
+        Visor(rendering_engine="invalid_engine")
+
+
+def test_initialization_with_standalone_flag():
+    """Test initialization with standalone flag."""
+    visor_instance = Visor(standalone=True)
+    assert visor_instance.standalone
+    assert visor_instance._server_manager._server._www.find("client_bundle") != -1
+
+
+def test_initialization_with_standalone_flag_false():
+    """Test initialization with standalone flag set to false."""
+    visor_instance = Visor(standalone=False)
+    assert not visor_instance.standalone
+    assert visor_instance._server_manager._server._www is None
+
+
+@pytest.mark.skip("Disabled checks due to having too many false negatives")
+def test_render_input_invalid_file():
+    """Test render_input with invalid file."""
+    visor_instance = Visor()
+    with pytest.raises(InvalidFileError):
+        visor_instance._Visor__render_input("invalid_file.txt", metadata)
+
+
+@pytest.mark.skip("Disabled checks due to having too many false negatives")
+def test_render_input_invalid_file_format():
+    """Test render_input with invalid file format."""
+    visor_instance = Visor()
+    with pytest.raises(InvalidFileFormatError):
+        visor_instance._Visor__render_input(plate_scdocx_file, metadata)
+
+
+def test_initiazation_url():
+    """Test initialization URL."""
+    visor_instance = Visor(url="http://localhost:8080")
+    assert visor_instance.url == "http://localhost:8080"
+
+
+def test_update_model_file():
+    """Test update API with VTU file after starting with VTP file."""
+    async def update_test():
+        visor_instance = Visor()
+        await visor_instance._start_async(input=plate_vtp_file, metadata=metadata)
+        visor_instance.update(mesh_vtu_file, metadata)
+        await visor_instance._stop_async()
+
+    asyncio.run(update_test())
+
+
+def test_update_model_data():
+    """Test update API with VTU file after starting with multiblock file."""
+    async def update_test():
+        multiblock = vtkMultiBlockDataSet()
+        visor_instance = Visor()
+        await visor_instance._start_async(input=multiblock, metadata=metadata)
+        visor_instance.update(mesh_vtu_file, metadata)
+        await visor_instance._stop_async()
+
+    asyncio.run(update_test())
+
+
+def test_start_with_invalid_timeout():
+    """Test that start with invalid timeout."""
+    visor_instance = Visor()
+    with pytest.raises(InvalidServerTimeoutError):
+        async def start():
+            await visor_instance._start_async(input=plate_vtp_file, timeout=-2)
+
+        asyncio.run(start())
+
+def test_save_load_state(tmp_path):
+    """Test that state can be saved and loaded."""
+    state_dir = path.join(tmp_path, "state")
+    makedirs(state_dir, exist_ok=True)
+    assert path.exists(state_dir)
+    assert path.isdir(state_dir)
+
+    visor_instance = Visor()
+
+    # New behavior: save_state awaits a frontend-backed `scene.get_state()`.
+    # Keep the persisted state minimal and aligned with PersistedViewerStateV1 schema.
+    mock_state = PersistedViewerStateV1()
+
+    async def fake_get_state(timeout: float = 5.0):
+        return mock_state
+
+    # `save_state` is async; it also requires the server to be "on" via decorator.
+    # `running` is a read-only property, so we mock the underlying server manager.
+    visor_instance._scene = MagicMock()
+    visor_instance._scene.get_state = MagicMock(side_effect=fake_get_state)
+    visor_instance._server_manager = MagicMock()
+    visor_instance._server_manager.running = True
+
+    asyncio.run(visor_instance.save_state(state_dir=state_dir))
+
+    state_file = path.join(state_dir, "visor.json")
+    assert path.exists(state_file)
+
+    # Sanity-check that the file round-trips into the same model.
+    with open(state_file, "r") as f:
+        loaded = PersistedViewerStateV1.model_validate_json(f.read())
+    assert loaded == mock_state
+
+def test_start():
+    """Test that start."""
+    async def start_test():
+        visor_instance = Visor()
+        print(f'visor_instance: {visor_instance.running}')
+        await visor_instance._start_async(input=vtm_file, metadata=metadata, timeout=5)
+        assert visor_instance._health()
+        await visor_instance._stop_async()
+
+    asyncio.run(start_test())
+
+def test_start_async_calls_render_input():
+    """Test that start async calls render input."""
+    async def run():
+        visor = VisorVTK()
+        with patch.object(visor._scene, "render") as mock_render:
+            with patch("os.path.exists", return_value=True):
+                await visor._start_async(input="dummy.vtp", metadata=Metadata(name="test", unit="m"))
+                mock_render.assert_called_once()
+    asyncio.run(run())
diff --git a/tests/integration/test_save_load_state.py b/tests/integration/test_save_load_state.py
new file mode 100644
index 00000000..f19be965
--- /dev/null
+++ b/tests/integration/test_save_load_state.py
@@ -0,0 +1,372 @@
+"""Integration tests for dataset snapshot save/load via VisorFileIO and VisorVTK.
+
+These tests use real VTK files and real filesystem I/O โ€” no mocking of readers,
+writers, or the VTK pipeline.  Only the Trame server manager and Trame app are
+patched out so no network/process resources are allocated.
+
+Scenarios covered
+-----------------
+write_dataset / read_dataset round-trip:
+    - PolyData, UnstructuredGrid, and MultiBlockDataSet snapshots survive a
+      write โ†’ read cycle with equal geometry.
+
+build_metadata_for_load_state:
+    - The ExtendedMetadata constructed for a load-state restore carries the
+      correct name, unit, file_path, metadata_path, and state.
+
+_load_datasets_from_state / load_state (VisorVTK):
+    - After write_dataset writes snapshots, a fresh interface with zero datasets
+      correctly loads each snapshot and ends up with the right datasets.
+    - Datasets are marked clean after restore.
+    - Missing / absent snapshot paths are skipped gracefully.
+    - Partial restores load only the available snapshots.
+    - Unit from state is preserved in the registry.
+    - load_state skips the restore when the scene already has datasets.
+
+Real test files used:
+    tests/files/plate.vtp                โ€” VTK PolyData
+    tests/files/mesh.vtu                 โ€” VTK UnstructuredGrid
+    tests/files/simple_multiblock.vtkhdf โ€” VTK MultiBlockDataSet
+"""
+
+import os
+from unittest.mock import MagicMock, patch
+
+import pytest
+from vtkmodules.vtkCommonDataModel import (
+    vtkMultiBlockDataSet,
+    vtkPolyData,
+    vtkUnstructuredGrid,
+)
+
+from ansys.visor.viewer.app.visor_vtk import VisorVTK
+from ansys.visor.viewer.core.metadata import ExtendedMetadata
+from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
+from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState
+from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1
+from ansys.visor.viewer.vtk.io.file_to_dataset import file_to_dataset
+from ansys.visor.viewer.vtk.io.visor_file_io import VisorFileIO
+
+# ------------------------------------------------------------------ #
+# Helpers
+# ------------------------------------------------------------------ #
+
+def _files_dir() -> str:
+    return os.path.join(os.path.dirname(__file__), "..", "files")
+
+def _vtp_path() -> str:
+    return os.path.join(_files_dir(), "plate.vtp")
+
+def _vtu_path() -> str:
+    return os.path.join(_files_dir(), "mesh.vtu")
+
+def _vtkhdf_multiblock_path() -> str:
+    return os.path.join(_files_dir(), "simple_multiblock.vtkhdf")
+
+
+# ------------------------------------------------------------------ #
+# Fixtures
+# ------------------------------------------------------------------ #
+
+@pytest.fixture
+def file_io():
+    """PyTest fixture for file io."""
+    return VisorFileIO()
+
+
+@pytest.fixture
+def iface():
+    """A VisorVTK with a real VTK scene but Trame server/app patched
+    out so no network or process resources are allocated."""
+    with patch("ansys.visor.viewer.app.visor_vtk.TrameServerManager") as mock_sm, \
+         patch("ansys.visor.viewer.app.visor_vtk_local.LocalApp"):
+        from trame.app import get_server
+        real_server = get_server()
+        mock_sm.return_value.server = real_server
+        instance = VisorVTK()
+    yield instance
+    try:
+        instance._scene.clear()
+        instance._scene.cleanup_state()
+    except Exception:
+        pass
+
+
+# ================================================================== #
+# write_dataset / read_dataset round-trips
+# ================================================================== #
+
+class TestWriteDatasetRoundTrip:
+    """write_dataset must produce a VTKHDF file that read_dataset can reload
+    with geometry identical to the original in-memory object."""
+
+    def test_polydata_round_trip_preserves_point_count(self, file_io, tmp_path):
+        """Test that polydata round trip preserves point count."""
+        original = file_to_dataset(_vtp_path())
+        dest = tmp_path / "plate_snapshot.vtkhdf"
+
+        file_io.write_dataset(dest, original)
+        assert dest.exists()
+
+        restored = file_io.read_dataset(str(dest))
+        assert isinstance(restored, vtkPolyData)
+        assert restored.GetNumberOfPoints() == original.GetNumberOfPoints()
+        assert restored.GetNumberOfCells() == original.GetNumberOfCells()
+
+    def test_unstructured_grid_round_trip_preserves_point_count(self, file_io, tmp_path):
+        """Test that unstructured grid round trip preserves point count."""
+        original = file_to_dataset(_vtu_path())
+        dest = tmp_path / "mesh_snapshot.vtkhdf"
+
+        file_io.write_dataset(dest, original)
+        assert dest.exists()
+
+        restored = file_io.read_dataset(str(dest))
+        assert isinstance(restored, vtkUnstructuredGrid)
+        assert restored.GetNumberOfPoints() == original.GetNumberOfPoints()
+        assert restored.GetNumberOfCells() == original.GetNumberOfCells()
+
+    def test_multiblock_round_trip_preserves_block_count(self, file_io, tmp_path):
+        """Test that multiblock round trip preserves block count."""
+        original = file_to_dataset(_vtkhdf_multiblock_path())
+        dest = tmp_path / "multiblock_snapshot.vtkhdf"
+
+        file_io.write_dataset(dest, original)
+        assert dest.exists()
+
+        restored = file_io.read_dataset(str(dest))
+        assert isinstance(restored, vtkMultiBlockDataSet)
+        assert restored.GetNumberOfBlocks() == original.GetNumberOfBlocks()
+
+    def test_snapshot_file_is_created_in_nested_directory(self, file_io, tmp_path):
+        """Test that snapshot file is created in nested directory."""
+        original = file_to_dataset(_vtp_path())
+        dest = tmp_path / "save" / "datasets" / "plate_snapshot.vtkhdf"
+
+        file_io.write_dataset(dest, original)
+
+        assert dest.exists()
+
+    def test_get_persisted_dataset_path_matches_written_file(self, file_io, tmp_path):
+        """The path returned by get_persisted_dataset_path must be exactly
+        where write_dataset puts the file."""
+        original = file_to_dataset(_vtp_path())
+        expected_path = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+
+        file_io.write_dataset(expected_path, original)
+
+        assert expected_path.exists()
+        restored = file_io.read_dataset(str(expected_path))
+        assert restored.GetNumberOfPoints() == original.GetNumberOfPoints()
+
+
+# ================================================================== #
+# build_metadata_for_load_state
+# ================================================================== #
+
+class TestBuildMetadataForLoadStateIntegration:
+    """build_metadata_for_load_state must produce ExtendedMetadata that wires
+    all fields correctly from real PersistedDatasetState values."""
+
+    def test_name_and_unit_are_preserved(self, file_io, tmp_path):
+        """Test that name and unit are preserved."""
+        ds_state = PersistedDatasetState(
+            serialized_dataset_path=str(tmp_path / "snap.vtkhdf")
+        )
+        meta = file_io.build_metadata_for_load_state("plate", "mm", ds_state)
+        assert meta.name == "plate"
+        assert meta.unit == "mm"
+
+    def test_source_file_path_is_propagated(self, file_io, tmp_path):
+        """Test that source file path is propagated."""
+        ds_state = PersistedDatasetState(
+            serialized_dataset_path=str(tmp_path / "snap.vtkhdf"),
+            source_file_path=_vtp_path(),
+        )
+        meta = file_io.build_metadata_for_load_state("plate", "m", ds_state)
+        assert meta.file_path == _vtp_path()
+
+    def test_source_metadata_path_is_propagated(self, file_io, tmp_path):
+        """Test that source metadata path is propagated."""
+        ds_state = PersistedDatasetState(
+            serialized_dataset_path=str(tmp_path / "snap.vtkhdf"),
+            source_metadata_path="/some/meta.json",
+        )
+        meta = file_io.build_metadata_for_load_state("plate", "m", ds_state)
+        assert meta.metadata_path == "/some/meta.json"
+
+    def test_state_field_carries_serialized_path(self, file_io, tmp_path):
+        """Test that state field carries serialized path."""
+        snap = str(tmp_path / "snap.vtkhdf")
+        ds_state = PersistedDatasetState(serialized_dataset_path=snap)
+        meta = file_io.build_metadata_for_load_state("model", "m", ds_state)
+        assert meta.state.serialized_dataset_path == snap
+
+    def test_none_unit_becomes_empty_string(self, file_io):
+        """Test that when unit=None is passed, unit becomes empty string."""
+        ds_state = PersistedDatasetState()
+        meta = file_io.build_metadata_for_load_state("model", None, ds_state)
+        assert meta.unit == ""
+
+
+# ================================================================== #
+# _load_datasets_from_state โ€” VisorScene
+# ================================================================== #
+
+class TestLoadDatasetsFromState:
+    """End-to-end: write real snapshots then verify _load_datasets_from_state
+    populates the scene correctly, and that load_state skips the restore when
+    datasets are already present."""
+
+    def _make_state(self, dataset_states: dict, unit: str = "m") -> PersistedViewerStateV1:
+        return PersistedViewerStateV1.from_components(
+            ui_state=VisorUIState(),
+            unit=unit,
+            orthographic_enabled=None,
+            cross_section_enabled=None,
+            edges_enabled=None,
+            bounding_box_enabled=None,
+            datasets=dataset_states,
+        )
+
+    def test_single_dataset_is_restored_into_empty_scene(self, file_io, iface, tmp_path):
+        """Test that single dataset is restored into empty scene."""
+        original = file_to_dataset(_vtp_path())
+        snap = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+        file_io.write_dataset(snap, original)
+
+        state = self._make_state({"plate": PersistedDatasetState(serialized_dataset_path=str(snap))})
+
+        assert iface._scene.dataset_count == 0
+        iface._load_datasets_from_state(state)
+
+        assert iface._scene.dataset_count == 1
+        assert next(iter(iface._scene.datasets.values())).name == "plate"
+
+    def test_restored_dataset_geometry_matches_original(self, file_io, iface, tmp_path):
+        """Test that restored dataset geometry matches original."""
+        original = file_to_dataset(_vtp_path())
+        snap = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+        file_io.write_dataset(snap, original)
+
+        state = self._make_state({"plate": PersistedDatasetState(serialized_dataset_path=str(snap))})
+        iface._load_datasets_from_state(state)
+
+        restored_data = next(iter(iface._scene.datasets.values())).data
+        assert restored_data.GetNumberOfPoints() == original.GetNumberOfPoints()
+        assert restored_data.GetNumberOfCells() == original.GetNumberOfCells()
+
+    def test_multiple_datasets_are_all_restored(self, file_io, iface, tmp_path):
+        """Test that multiple datasets are all restored."""
+        vtp = file_to_dataset(_vtp_path())
+        vtu = file_to_dataset(_vtu_path())
+        snap_vtp = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+        snap_vtu = file_io.get_persisted_dataset_path(str(tmp_path), "mesh")
+        file_io.write_dataset(snap_vtp, vtp)
+        file_io.write_dataset(snap_vtu, vtu)
+
+        state = self._make_state({
+            "plate": PersistedDatasetState(serialized_dataset_path=str(snap_vtp)),
+            "mesh":  PersistedDatasetState(serialized_dataset_path=str(snap_vtu)),
+        })
+        iface._load_datasets_from_state(state)
+
+        assert iface._scene.dataset_count == 2
+        names = {ds.name for ds in iface._scene.datasets.values()}
+        assert names == {"plate", "mesh"}
+
+    def test_restored_dataset_is_marked_clean(self, file_io, iface, tmp_path):
+        """Test that restored dataset is marked clean."""
+        original = file_to_dataset(_vtp_path())
+        snap = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+        file_io.write_dataset(snap, original)
+
+        state = self._make_state({"plate": PersistedDatasetState(serialized_dataset_path=str(snap))})
+        iface._load_datasets_from_state(state)
+
+        dataset = next(iter(iface._scene.datasets.values()))
+        assert dataset.is_dirty is False
+
+    def test_missing_snapshot_is_skipped_gracefully(self, iface, tmp_path):
+        """Test that missing snapshot is skipped gracefully."""
+        state = self._make_state({
+            "plate": PersistedDatasetState(
+                serialized_dataset_path=str(tmp_path / "nonexistent_snapshot.vtkhdf")
+            )
+        })
+        iface._load_datasets_from_state(state)
+        assert iface._scene.dataset_count == 0
+
+    def test_dataset_with_no_snapshot_path_is_skipped(self, iface):
+        """Test that dataset with no snapshot path is skipped."""
+        state = self._make_state({"plate": PersistedDatasetState(serialized_dataset_path=None)})
+        iface._load_datasets_from_state(state)
+        assert iface._scene.dataset_count == 0
+
+    def test_partial_restore_loads_available_snapshots(self, file_io, iface, tmp_path):
+        """When one snapshot exists and one is missing, only the present one is loaded."""
+        original = file_to_dataset(_vtp_path())
+        snap = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+        file_io.write_dataset(snap, original)
+
+        state = self._make_state({
+            "plate": PersistedDatasetState(serialized_dataset_path=str(snap)),
+            "mesh":  PersistedDatasetState(serialized_dataset_path=str(tmp_path / "nonexistent.vtkhdf")),
+        })
+        iface._load_datasets_from_state(state)
+
+        assert iface._scene.dataset_count == 1
+        assert next(iter(iface._scene.datasets.values())).name == "plate"
+
+    def test_restore_preserves_unit_from_state(self, file_io, iface, tmp_path):
+        """Test that restoring preserves unit from state."""
+        original = file_to_dataset(_vtp_path())
+        snap = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+        file_io.write_dataset(snap, original)
+
+        state = self._make_state({"plate": PersistedDatasetState(serialized_dataset_path=str(snap))}, unit="mm")
+        iface._load_datasets_from_state(state)
+
+        assert iface._scene._dataset_registry.unit == "mm"
+
+    def test_load_state_skips_restore_when_scene_already_has_datasets(self, file_io, iface, tmp_path):
+        """load_state must skip _load_datasets_from_state when dataset_count > 0."""
+        # Pre-populate with one real dataset
+        existing = file_to_dataset(_vtp_path())
+        iface._scene.add_dataset(existing, ExtendedMetadata(name="existing", unit="m"))
+        assert iface._scene.dataset_count == 1
+
+        # Write a second snapshot + visor.json that load_state should NOT load
+        original = file_to_dataset(_vtu_path())
+        snap = file_io.get_persisted_dataset_path(str(tmp_path), "mesh")
+        file_io.write_dataset(snap, original)
+        state = self._make_state({"mesh": PersistedDatasetState(serialized_dataset_path=str(snap))})
+        file_io.write_state(str(tmp_path), state)
+
+        # Stub apply_state so the frontend bridge isn't called
+        iface._scene.apply_state = MagicMock()
+        iface.load_state(str(tmp_path))
+
+        # Only the pre-existing dataset should be present
+        assert iface._scene.dataset_count == 1
+        assert next(iter(iface._scene.datasets.values())).name == "existing"
+
+    def test_stale_snapshot_deleted_after_dataset_removed(self, file_io, tmp_path):
+        """A snapshot written during a previous save_state must be deleted when
+        the corresponding dataset is no longer in the new state."""
+        # Simulate a previous save: two snapshots + visor.json on disk
+        plate_snap = file_io.get_persisted_dataset_path(str(tmp_path), "plate")
+        mesh_snap  = file_io.get_persisted_dataset_path(str(tmp_path), "mesh")
+        plate_snap.touch()
+        mesh_snap.touch()
+
+        # New state only references "plate" โ€” "mesh" has been removed
+        new_state = self._make_state({
+            "plate": PersistedDatasetState(serialized_dataset_path=str(plate_snap)),
+        })
+        file_io.write_state(str(tmp_path), new_state)
+        file_io.delete_stale_snapshots(str(tmp_path), new_state)
+
+        assert plate_snap.exists()
+        assert not mesh_snap.exists()
+
diff --git a/tests/integration/test_service_mock.py b/tests/integration/test_service_mock.py
new file mode 100644
index 00000000..b59748fc
--- /dev/null
+++ b/tests/integration/test_service_mock.py
@@ -0,0 +1,102 @@
+from os import path
+from unittest.mock import AsyncMock, Mock, patch
+
+import pytest
+from fastapi.testclient import TestClient
+from httpx import ASGITransport, AsyncClient
+
+from ansys.visor.viewer.api.server import app
+from ansys.visor.viewer.core.metadata import Metadata
+
+client = TestClient(app)
+client_path = path.join(path.dirname(path.dirname(__file__)),'src','ansys','visor',"visor-client","dist")
+input = path.join(path.dirname(__file__), "files","plate.vtp")
+metadata = Metadata(name="test_model", unit="m")
+
+
+def test_root():
+    """Test root endpoint."""
+    response = client.get("/")
+    assert response.status_code == 200
+    assert "http://localhost:8081" in response.json()['urls']
+
+def test_info():
+    """Test info API."""
+    response = client.get("/info")
+    assert response.status_code == 200
+    assert response.json()['app_name'] == 'Visor Viewer'
+
+def test_initialize():
+    """Test initialize API."""
+    response = client.post("/initialize",json={"host":"localhost","port":8081,"standalone":False})
+    assert response.status_code == 200
+    assert response.json()['message'] == 'Set active instance to http://localhost:8081'
+
+
+@pytest.mark.anyio
+async def test_start_endpoint_calls_start_async_with_args():
+    """Test that start endpoint calls start async with args."""
+    for route in app.routes:
+        if route.path == "/start":
+            visor_api_instance = route.endpoint.__self__
+            break
+    else:
+        raise RuntimeError("Could not find VisorAPI instance for /start route")
+
+    test_file_path = "some/test/file.vtu"
+    test_timeout = 42
+    with patch.object(
+        visor_api_instance.visualizer, "_start_async", AsyncMock(return_value=("dataset_id", "task"))
+    ) as mock_start_async:
+        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
+            response = await ac.post("/start", json={
+                "file_path": test_file_path,
+                "metadata": {"name": "model", "unit": "m"},
+                "timeout": test_timeout
+            })
+        assert response.status_code in (200, 500)
+        mock_start_async.assert_awaited_once_with(
+            input=test_file_path, metadata=Metadata(name='model', unit='m'), timeout=test_timeout
+        )
+
+@pytest.mark.anyio
+async def test_update_endpoint_calls_update_with_file_path():
+    """Test that update endpoint calls update with file path."""
+    for route in app.routes:
+        if route.path == "/update":
+            visor_api_instance = route.endpoint.__self__
+            break
+    else:
+        raise RuntimeError("Could not find VisorAPI instance for /update route")
+
+    test_file_path = "some/test/file.vtu"
+    test_metadata = Metadata(name='model', unit='m')
+    with patch.object(
+            visor_api_instance.visualizer, "update", Mock(return_value=1)
+    ) as mock_update:
+        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
+            response = await ac.post("/update", json={
+                "file_path": test_file_path,
+                "metadata": {"name": "model", "unit": "m"}
+            })
+        assert response.status_code in (200, 500)
+        mock_update.assert_called_once_with(test_file_path, test_metadata)
+
+@pytest.mark.anyio
+async def test_stop_endpoint_calls_stop_instance():
+    """Test that stop endpoint calls stop instance."""
+    # Find the VisorAPI instance
+    for route in app.routes:
+        if route.path == "/stop_visualization":
+            visor_api_instance = route.endpoint.__self__
+            break
+    else:
+        raise RuntimeError("Could not find VisorAPI instance for /stop_visualization route")
+
+    with patch.object(
+        visor_api_instance.visualizer, "_stop_async", AsyncMock(return_value=None)
+    ) as mock_stop_async:
+        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
+            response = await ac.post("/stop_visualization")
+        assert response.status_code in (200, 500)
+        mock_stop_async.assert_awaited_once()
\ No newline at end of file
diff --git a/tests/integration/test_visor_file_io.py b/tests/integration/test_visor_file_io.py
new file mode 100644
index 00000000..f315627e
--- /dev/null
+++ b/tests/integration/test_visor_file_io.py
@@ -0,0 +1,374 @@
+"""Integration tests for VisorFileIO.
+
+These tests exercise real filesystem I/O โ€” no mocking of ``open``, ``Path``,
+or ``file_to_dataset``.  pytest's ``tmp_path`` fixture provides a clean
+temporary directory for each test.
+
+Real test files used:
+    tests/files/plate.vtp                โ€” VTK PolyData (legacy XML)
+    tests/files/mesh.vtu                 โ€” VTK UnstructuredGrid (legacy XML)
+    tests/files/plate.vtkhdf             โ€” VTK PolyData (VTKHDF)
+    tests/files/mesh.vtkhdf              โ€” VTK UnstructuredGrid (VTKHDF)
+    tests/files/simple_multiblock.vtkhdf โ€” VTK MultiBlockDataSet (VTKHDF)
+"""
+import json
+import os
+
+import pytest
+from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet, vtkPolyData, vtkUnstructuredGrid
+
+from ansys.visor.viewer.core.metadata import Metadata
+from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1
+from ansys.visor.viewer.vtk.io.visor_file_io import _STATE_FILE_NAME, VisorFileIO
+
+# ------------------------------------------------------------------ #
+# Helpers
+# ------------------------------------------------------------------ #
+
+def _test_files_dir() -> str:
+    """Return the path to the directory containing test files."""
+    return os.path.join(os.path.dirname(__file__), "..", "files")
+
+def _vtp_path() -> str:
+    """Return the path to the test VTP file."""
+    return os.path.join(_test_files_dir(), "plate.vtp")
+
+def _vtu_path() -> str:
+    """Return the path to the test VTU file."""
+    return os.path.join(_test_files_dir(), "mesh.vtu")
+
+def _vtkhdf_polydata_path() -> str:
+    """Return the path to the test VTKHDF PolyData file."""
+    return os.path.join(_test_files_dir(), "plate.vtkhdf")
+
+def _vtkhdf_unstructured_grid_path() -> str:
+    """Return the path to the test VTKHDF UnstructuredGrid file."""
+    return os.path.join(_test_files_dir(), "mesh.vtkhdf")
+
+def _vtkhdf_multiblock_path() -> str:
+    """Return the path to the test VTKHDF MultiBlockDataSet file."""
+    return os.path.join(_test_files_dir(), "simple_multiblock.vtkhdf")
+
+
+# ------------------------------------------------------------------ #
+# Fixtures
+# ------------------------------------------------------------------ #
+
+@pytest.fixture
+def file_io():
+    """PyTest fixture for file io."""
+    return VisorFileIO()
+
+@pytest.fixture
+def minimal_state():
+    """PyTest fixture for minimal state."""
+    return PersistedViewerStateV1()
+
+@pytest.fixture
+def metadata_file(tmp_path):
+    """Write a valid metadata JSON file to tmp_path and return its path."""
+    meta = {"name": "test_model", "unit": "m"}
+    path = tmp_path / "meta.json"
+    path.write_bytes(json.dumps(meta).encode())
+    return str(path)
+
+
+# ================================================================== #
+# read_dataset
+# ================================================================== #
+
+class TestReadDatasetIntegration:
+    def test_reads_vtp_file(self, file_io):
+        """Test reading VTP file."""
+        result = file_io.read_dataset(_vtp_path())
+        assert result is not None
+        assert result.GetNumberOfPoints() > 0
+
+    def test_reads_vtu_file(self, file_io):
+        """Test reading VTU file."""
+        result = file_io.read_dataset(_vtu_path())
+        assert result is not None
+        assert result.GetNumberOfPoints() > 0
+
+    def test_raises_for_missing_file(self, file_io):
+        """Test that missing file raises RuntimeError."""
+        with pytest.raises(RuntimeError, match="file does not exist"):
+            file_io.read_dataset("/nonexistent/path/model.vtp")
+
+    def test_raises_for_unsupported_extension(self, file_io, tmp_path):
+        """Test that unsupported extension raises RuntimeError."""
+        bad_file = tmp_path / "model.xyz"
+        bad_file.write_text("dummy")
+        with pytest.raises(RuntimeError, match="Unsupported file"):
+            file_io.read_dataset(str(bad_file))
+
+
+# ================================================================== #
+# read_metadata
+# ================================================================== #
+
+class TestReadMetadataIntegration:
+    def test_reads_valid_metadata_file(self, file_io, metadata_file):
+        """Test that a valid metadata file is read correctly."""
+        result = file_io.read_metadata(metadata_file)
+        assert isinstance(result, Metadata)
+        assert result.name == "test_model"
+        assert result.unit == "m"
+
+    def test_raises_for_missing_file(self, file_io):
+        """Test that missing file raises FileNotFoundError."""
+        with pytest.raises(FileNotFoundError):
+            file_io.read_metadata("/nonexistent/meta.json")
+
+    def test_raises_for_invalid_json(self, file_io, tmp_path):
+        """Test that invalid json raises ValueError."""
+        bad = tmp_path / "bad.json"
+        bad.write_bytes(b"not valid json")
+        with pytest.raises(json.JSONDecodeError):
+            file_io.read_metadata(str(bad))
+
+
+# ================================================================== #
+# write_state / read_state round-trip
+# ================================================================== #
+
+class TestWriteReadStateIntegration:
+    def test_write_creates_visor_json(self, file_io, minimal_state, tmp_path):
+        """Test that write_state creates visor json file."""
+        file_io.write_state(str(tmp_path), minimal_state)
+        assert (tmp_path / _STATE_FILE_NAME).exists()
+
+    def test_write_creates_directory_if_absent(self, file_io, minimal_state, tmp_path):
+        """Test that write_state creates directory if absent."""
+        state_dir = str(tmp_path / "nested" / "dir")
+        file_io.write_state(state_dir, minimal_state)
+        assert (tmp_path / "nested" / "dir" / _STATE_FILE_NAME).exists()
+
+    def test_written_file_is_valid_json(self, file_io, minimal_state, tmp_path):
+        """Test that write_state writes a valid json."""
+        file_io.write_state(str(tmp_path), minimal_state)
+        content = (tmp_path / _STATE_FILE_NAME).read_text()
+        parsed = json.loads(content)
+        assert "version" in parsed
+
+    def test_round_trip_preserves_version(self, file_io, minimal_state, tmp_path):
+        """Test that round trip preserves version."""
+        file_io.write_state(str(tmp_path), minimal_state)
+        result = file_io.read_state(str(tmp_path))
+        assert result.version == minimal_state.version
+
+    def test_round_trip_preserves_ui_state(self, file_io, tmp_path):
+        """Test that round trip preserves ui state."""
+        from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
+        state = PersistedViewerStateV1(ui=VisorUIState(dark_theme=True))
+        file_io.write_state(str(tmp_path), state)
+        result = file_io.read_state(str(tmp_path))
+        assert result.ui.dark_theme is True
+
+    def test_write_returns_path_to_file(self, file_io, minimal_state, tmp_path):
+        """Test that write_state returns path to file."""
+        result = file_io.write_state(str(tmp_path), minimal_state)
+        assert result == tmp_path / _STATE_FILE_NAME
+        assert result.exists()
+
+    def test_read_raises_file_not_found_when_missing(self, file_io, tmp_path):
+        """Test that read_state raises FileNotFoundError when missing."""
+        with pytest.raises(FileNotFoundError, match="State file not found"):
+            file_io.read_state(str(tmp_path))
+
+    def test_read_raises_for_corrupt_json(self, file_io, tmp_path):
+        """Test that read_state raises for corrupt json."""
+        (tmp_path / _STATE_FILE_NAME).write_text("not valid json")
+        with pytest.raises(Exception):
+            file_io.read_state(str(tmp_path))
+
+    def test_write_overwrites_existing_state_file(self, file_io, tmp_path):
+        """Test that write_state overwrites existing state file."""
+        from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
+        state_a = PersistedViewerStateV1(ui=VisorUIState(dark_theme=False))
+        state_b = PersistedViewerStateV1(ui=VisorUIState(dark_theme=True))
+        file_io.write_state(str(tmp_path), state_a)
+        file_io.write_state(str(tmp_path), state_b)
+        result = file_io.read_state(str(tmp_path))
+        assert result.ui.dark_theme is True
+
+
+# ================================================================== #
+# vtkhdf read support โ€” read_dataset (VisorFileIO wrapper)
+# ================================================================== #
+
+class TestReadDatasetVtkhdfIntegration:
+    """read_dataset must handle all three supported vtkhdf dataset types
+    without requiring callers to know which reader is used internally."""
+
+    def test_reads_vtkhdf_polydata_file(self, file_io):
+        """Test that read_dataset reads vtkhdf polydata file."""
+        result = file_io.read_dataset(_vtkhdf_polydata_path())
+        assert isinstance(result, vtkPolyData)
+        assert result.GetNumberOfPoints() > 0
+
+    def test_reads_vtkhdf_unstructured_grid_file(self, file_io):
+        """Test that read_dataset reads vtkhdf unstructured grid file."""
+        result = file_io.read_dataset(_vtkhdf_unstructured_grid_path())
+        assert isinstance(result, vtkUnstructuredGrid)
+        assert result.GetNumberOfPoints() > 0
+
+    def test_reads_vtkhdf_multiblock_file(self, file_io):
+        """Test that read_dataset reads vtkhdf multiblock file."""
+        result = file_io.read_dataset(_vtkhdf_multiblock_path())
+        assert isinstance(result, vtkMultiBlockDataSet)
+        assert result.GetNumberOfBlocks() > 0
+
+    def test_vtkhdf_polydata_point_count_matches_vtp(self, file_io):
+        """VTKHDF and VTP versions of the same file must have equal geometry."""
+        vtp = file_io.read_dataset(_vtp_path())
+        vtkhdf = file_io.read_dataset(_vtkhdf_polydata_path())
+        assert vtkhdf.GetNumberOfPoints() == vtp.GetNumberOfPoints()
+        assert vtkhdf.GetNumberOfCells() == vtp.GetNumberOfCells()
+
+    def test_vtkhdf_unstructured_grid_point_count_matches_vtu(self, file_io):
+        """VTKHDF and VTU versions of the same file must have equal geometry."""
+        vtu = file_io.read_dataset(_vtu_path())
+        vtkhdf = file_io.read_dataset(_vtkhdf_unstructured_grid_path())
+        assert vtkhdf.GetNumberOfPoints() == vtu.GetNumberOfPoints()
+        assert vtkhdf.GetNumberOfCells() == vtu.GetNumberOfCells()
+
+
+# ================================================================== #
+# resolve_metadata โ€” name inferred from file path (vtkhdf paths)
+# ================================================================== #
+
+class TestResolveMetadataVtkhdfIntegration:
+    """When no metadata object or path is provided, the dataset name must be
+    inferred from the file stem of the input path โ€” including .vtkhdf paths."""
+
+    def test_name_inferred_from_vtkhdf_polydata_path(self, file_io):
+        """Test name inferred from vtkhdf polydata path."""
+        _, ext_meta = file_io.resolve(None, None)
+        # Call resolve_metadata directly with a vtkhdf path as the input hint
+        result = file_io.resolve_metadata(None, _vtkhdf_polydata_path())
+        assert result.name == "plate"
+
+    def test_name_inferred_from_vtkhdf_unstructured_grid_path(self, file_io):
+        """Test name inferred from vtkhdf unstructured grid path."""
+        result = file_io.resolve_metadata(None, _vtkhdf_unstructured_grid_path())
+        assert result.name == "mesh"
+
+    def test_name_inferred_from_vtkhdf_multiblock_path(self, file_io):
+        """Test name inferred from vtkhdf multiblock path."""
+        result = file_io.resolve_metadata(None, _vtkhdf_multiblock_path())
+        assert result.name == "simple_multiblock"
+
+    def test_explicit_metadata_overrides_path_inference(self, file_io):
+        """An explicitly supplied Metadata must take priority over path inference."""
+        explicit = Metadata(name="my_custom_name", unit="mm")
+        result = file_io.resolve_metadata(explicit, _vtkhdf_polydata_path())
+        assert result.name == "my_custom_name"
+        assert result.unit == "mm"
+
+    def test_resolve_end_to_end_with_vtkhdf_input_sets_name_and_file_path(self, file_io):
+        """Full resolve() call with a vtkhdf path and no metadata must populate
+        both the inferred name and file_path on the returned ExtendedMetadata."""
+        path = _vtkhdf_polydata_path()
+        dataset, ext_meta = file_io.resolve(path, None)
+        assert isinstance(dataset, vtkPolyData)
+        assert ext_meta.name == "plate"
+        assert ext_meta.file_path == path
+
+
+# ================================================================== #
+# File handle released after reading (Windows regression)
+# ================================================================== #
+
+class TestFileHandleReleased:
+    """After file_to_dataset (or VisorFileIO.read_dataset) returns, the source
+    file must no longer be locked โ€” it must be possible to overwrite and delete
+    it without error.  This is the regression test for the WinError 32 bug
+    where vtkHDFReader kept the HDF5 file open as long as the dataset was alive.
+    """
+
+    def test_vtkhdf_file_can_be_overwritten_after_read(self, file_io, tmp_path):
+        """Test that vtkhdf file can be overwritten after read."""
+        import shutil
+        src = _vtkhdf_polydata_path()
+        copy = tmp_path / "plate_copy.vtkhdf"
+        shutil.copy2(src, copy)
+
+        dataset = file_io.read_dataset(str(copy))
+        assert dataset.GetNumberOfPoints() > 0
+
+        # Must not raise WinError 32 on Windows
+        copy.write_bytes(b"overwritten")
+        assert copy.read_bytes() == b"overwritten"
+
+    def test_vtkhdf_file_can_be_deleted_after_read(self, file_io, tmp_path):
+        """Test that vtkhdf file can be deleted after read."""
+        import shutil
+        src = _vtkhdf_polydata_path()
+        copy = tmp_path / "plate_copy.vtkhdf"
+        shutil.copy2(src, copy)
+
+        dataset = file_io.read_dataset(str(copy))
+        assert dataset.GetNumberOfPoints() > 0
+
+        # Must not raise WinError 32 on Windows
+        copy.unlink()
+        assert not copy.exists()
+
+    def test_vtkhdf_unstructured_grid_file_can_be_deleted_after_read(self, file_io, tmp_path):
+        """Test that vtkhdf unstructured grid file can be deleted after read."""
+        import shutil
+        src = _vtkhdf_unstructured_grid_path()
+        copy = tmp_path / "mesh_copy.vtkhdf"
+        shutil.copy2(src, copy)
+
+        dataset = file_io.read_dataset(str(copy))
+        assert dataset.GetNumberOfPoints() > 0
+
+        copy.unlink()
+        assert not copy.exists()
+
+    def test_vtkhdf_multiblock_snapshot_can_be_deleted_after_load(self, file_io, tmp_path):
+        """read_snapshot (used by load_state) must leave the original file unlocked
+        so that save_state can delete it as a stale snapshot."""
+        import shutil
+
+        from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
+        from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1
+
+        src = _vtkhdf_multiblock_path()
+        copy = tmp_path / "mb_copy.vtkhdf"
+        shutil.copy2(src, copy)
+
+        # Use read_snapshot โ€” the load_state path โ€” not read_dataset
+        dataset = file_io.read_snapshot(str(copy))
+        assert dataset.GetNumberOfBlocks() > 0
+
+        state = PersistedViewerStateV1.from_components(
+            ui_state=VisorUIState(), unit="m", orthographic_enabled=None,
+            cross_section_enabled=None, edges_enabled=None, bounding_box_enabled=None,
+            datasets={}
+        )
+        deleted = file_io.delete_stale_snapshots(str(tmp_path), state)
+
+        assert not copy.exists()
+        assert copy in deleted
+
+    def test_dataset_data_is_intact_after_file_deleted(self, file_io, tmp_path):
+        """The deep-copied dataset must retain all geometry even after the source
+        file has been removed from disk."""
+        import shutil
+        src = _vtkhdf_polydata_path()
+        original = file_io.read_dataset(src)
+        expected_points = original.GetNumberOfPoints()
+        expected_cells  = original.GetNumberOfCells()
+
+        copy = tmp_path / "plate_copy.vtkhdf"
+        shutil.copy2(src, copy)
+        dataset = file_io.read_dataset(str(copy))
+        copy.unlink()
+
+        assert dataset.GetNumberOfPoints() == expected_points
+        assert dataset.GetNumberOfCells()  == expected_cells
+
+
diff --git a/tests/integration/test_visor_local_scene.py b/tests/integration/test_visor_local_scene.py
new file mode 100644
index 00000000..10aa8797
--- /dev/null
+++ b/tests/integration/test_visor_local_scene.py
@@ -0,0 +1,256 @@
+# python
+# File: `tests/integration/test_visor_local_scene.py`
+import json
+import math
+import os
+from unittest.mock import PropertyMock, patch
+
+import pytest
+from trame.app import get_server
+
+from ansys.visor.viewer.core.metadata import ExtendedMetadata
+from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType
+from ansys.visor.viewer.vtk.io.file_to_dataset import file_to_dataset
+from ansys.visor.viewer.vtk.scene.local_scene import VisorLocalScene
+from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate
+
+
+@pytest.fixture
+def pipeline_instance():
+    """PyTest fixture for pipeline instance."""
+    server = get_server()
+    assert server is not None, "get_server() returned None"
+    scene = VisorLocalScene(server)
+    yield scene
+    try:
+        scene.clear()
+        scene.cleanup_state()
+    except Exception:
+        pass
+
+
+def get_test_file():
+    """Get the path to the test VTP file."""
+    return os.path.join(os.path.dirname(__file__), "..", "files", "plate.vtp")
+
+
+def test_initialization(pipeline_instance):
+    """Test pipeline initialization."""
+    assert pipeline_instance is not None
+    assert pipeline_instance.dataset_count == 0
+    assert pipeline_instance._renderer is not None
+    assert pipeline_instance._renderer._render_window is not None
+    assert pipeline_instance._renderer._render_window_interactor is not None
+    assert pipeline_instance._server is not None
+    assert pipeline_instance._scene_graph is not None
+    # Avoid dict-like membership checks on trame state to prevent iteration side effects
+
+
+def test_add_and_remove_dataset(pipeline_instance):
+    """Test that adding and removing a dataset works."""
+    file = get_test_file()
+    dataset = file_to_dataset(file)
+    metadata = ExtendedMetadata(name="test_model", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    assert pipeline_instance.dataset_count == 1
+    info = pipeline_instance.list_all_dataset_info()
+    dataset_id = next(iter(info.keys()))
+    pipeline_instance.remove_dataset(dataset_id)
+    assert pipeline_instance.dataset_count == 0
+
+
+def test_add_dataset_none_input(pipeline_instance):
+    """Test add_dataset raises ValueError when input is None."""
+    with pytest.raises(ValueError):
+        pipeline_instance.add_dataset(None, ExtendedMetadata(name="test", unit="m"))
+
+
+def test_list_dataset_state_info(pipeline_instance):
+    """Test that list_dataset_state_info works."""
+    file = get_test_file()
+    dataset = file_to_dataset(file)
+    metadata = ExtendedMetadata(name="modelA", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    info = pipeline_instance.list_all_dataset_info()
+    assert isinstance(info, dict)
+    assert len(info) == 1
+    did = next(iter(info.keys()))
+    assert info[did]["name"] == "modelA"
+
+
+def test_get_scene_details_and_json(pipeline_instance):
+    """Test that get_scene_details and get_scene_details_json work."""
+    file = get_test_file()
+    dataset = file_to_dataset(file)
+    metadata = ExtendedMetadata(name="test_model", unit="mm")
+    pipeline_instance.add_dataset(dataset, metadata)
+    details = pipeline_instance.get_scene_details()
+    assert details.app_state.scene.unit == "mm"
+    payload = json.loads(pipeline_instance.get_scene_details_json())
+    assert payload["appState"]["scene"]["unit"] == "mm"
+    assert "vtkInfo" in payload
+    assert "appState" in payload
+
+
+def test_clear_resets_registry_and_graph(pipeline_instance):
+    """Test that clear() resets registry and graph."""
+    file = get_test_file()
+    dataset = file_to_dataset(file)
+    metadata = ExtendedMetadata(name="m1", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    assert pipeline_instance.dataset_count == 1
+    pipeline_instance.populate_scene()
+    pipeline_instance.clear()
+    assert pipeline_instance.dataset_count == 0
+    assert pipeline_instance._scene_graph is None
+
+
+def test_cleanup_state_removes_wasm_keys(pipeline_instance):
+    """Test that cleanup_state removes wasm keys."""
+    with patch.object(type(pipeline_instance._server), "state", new_callable=PropertyMock) as mock_state:
+        mock_state.return_value = {}
+        pipeline_instance._server.state["wasm_ids"] = {"dummy": 1}
+        pipeline_instance._server.state["wasm_ref_name"] = {"dummy": 2}
+        pipeline_instance.cleanup_state()
+        assert "wasm_ids" not in pipeline_instance._server.state
+        assert "wasm_ref_name" not in pipeline_instance._server.state
+
+
+def test_update_widgets_without_scene_graph_raises(pipeline_instance):
+    """Test that update_widgets without scene graph raises RuntimeError."""
+    pipeline_instance._scene_graph = None
+    with pytest.raises(RuntimeError):
+        pipeline_instance.update_widgets()
+
+
+def test_update_widgets_with_scene_graph(pipeline_instance):
+    """Test that updating widgets with scene graph."""
+    file = get_test_file()
+    dataset = file_to_dataset(file)
+    metadata = ExtendedMetadata(name="m1", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    pipeline_instance.populate_scene()  # Should update widgets without error
+
+
+def test_reset_camera_without_scene_graph_no_raise(pipeline_instance):
+    """Test that resetting camera without scene graph does not raise any exception."""
+    pipeline_instance._scene_graph = None
+    pipeline_instance.reset_camera()  # Should not raise
+
+
+def test_reset_camera_with_scene_graph_applies_bounds(pipeline_instance):
+    """Test that resetting camera with scene graph results in bounds applied to renderer."""
+    pipeline_instance.reset_camera()  # Bounds applied to renderer
+
+
+def test_render_triggers_updates(pipeline_instance):
+    """Test that rendering does not raise any exception."""
+    pipeline_instance.render()  # Should not raise
+
+
+def test_remove_dataset_error_when_no_graph(pipeline_instance):
+    """Test that removing dataset with null scene graph raises RuntimeError."""
+    pipeline_instance._scene_graph = None
+    with pytest.raises(RuntimeError):
+        pipeline_instance.remove_dataset(1)
+
+
+def test_list_and_update_variables(pipeline_instance):
+    """Test that list_variables_for_dataset and update_variables_for_dataset works."""
+    file = get_test_file()
+    dataset = file_to_dataset(file)
+    metadata = ExtendedMetadata(name="m1", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    info = pipeline_instance.list_all_dataset_info()
+    did = next(iter(info.keys()))
+
+    before = pipeline_instance.list_variables_for_dataset(did)
+    assert isinstance(before, list)
+
+    num_points = dataset.GetPointData().GetArray("Normal").GetNumberOfTuples()
+    num_components = dataset.GetPointData().GetArray("Normal").GetNumberOfComponents()
+    # Minimal valid VisorVariableUpdate: scalar with one component and small data payload
+    updates = [
+        VisorVariableUpdate.from_dict({
+            "name":"Normal",
+            "type":"point",
+            "num_components":num_components,
+            "data":[0.0]*num_points*num_components,
+    })
+    ]
+    pipeline_instance.update_variables_for_dataset(did, updates)
+
+    after = pipeline_instance.list_variables_for_dataset(did)
+    assert isinstance(after, list)
+    # list_variables now returns one VisorPartVariables entry per part;
+    # non-composite datasets have exactly one part.
+    assert len(after) == 1
+    part = after[0]
+    assert len(part.variables) == 3
+    assert part.variables[0].name == "Normal"
+    assert part.variables[0].type == VisorVtkVariableType.POINT
+    assert part.variables[0].num_components == num_components
+
+
+def test_pick_geometry(pipeline_instance):
+    """Test that pick_geometry works."""
+    file = get_test_file()
+    dataset = file_to_dataset(file)
+    metadata = ExtendedMetadata(name="pick_test", unit="m")
+    dataset_id = pipeline_instance.add_dataset(dataset, metadata)
+    pipeline_instance.populate_scene()
+    # ensure VTK pipeline has produced outputs
+    pipeline_instance.render()
+
+    dataset_node = pipeline_instance._scene_graph.get_descendant_node(dataset_id)
+    assert dataset_node is not None
+    actors = [
+        pipeline_instance._renderer._pipelines[part.id].actor
+        for part in dataset_node.get_descendant_part_nodes(include_self=True)
+        if part.id in pipeline_instance._renderer._pipelines
+    ]
+    assert len(actors) > 0
+    # choose an actor whose mapper input dataset contains cells
+    actor = None
+    for a in actors:
+        try:
+            ds = a.GetMapper().GetInputDataObject(0, 0)
+            if ds is not None and ds.GetNumberOfCells() > 0:
+                actor = a
+                pick_ds = ds
+                break
+        except Exception:
+            continue
+    assert actor is not None
+    actor_wasm_id = pipeline_instance._renderer._object_manager.GetId(actor)
+
+    # Test face pick for first cell
+    res_face = pipeline_instance.pick_geometry(actor_wasm_id, 0, 'face', 0.0, 0.0, 0.0)
+    assert res_face["found"] is True
+    assert res_face["mode"] == "face"
+    assert "area" in res_face and res_face["area"] > 0
+    assert len(res_face["points"]) >= 3
+
+    # Test vertex pick using the first point of cell 0
+    # Use the mapper's dataset for cell/point selection
+    cell = pick_ds.GetCell(0)
+    pts = cell.GetPoints()
+    p0 = pts.GetPoint(0)
+    res_vertex = pipeline_instance.pick_geometry(actor_wasm_id, 0, 'vertex', p0[0], p0[1], p0[2])
+    assert res_vertex["found"] is True
+    assert res_vertex["mode"] == "vertex"
+    assert tuple(res_vertex["position"]) == tuple(p0)
+
+    # Test edge pick for first edge (p0 -> p1) using midpoint
+    if cell.GetNumberOfPoints() >= 2:
+        p1 = pts.GetPoint(1)
+        mid = ((p0[0] + p1[0]) / 2.0, (p0[1] + p1[1]) / 2.0, (p0[2] + p1[2]) / 2.0)
+        res_edge = pipeline_instance.pick_geometry(actor_wasm_id, 0, 'edge', mid[0], mid[1], mid[2])
+        assert res_edge["found"] is True
+        assert res_edge["mode"] == "edge"
+        assert tuple(res_edge["pointA"]) == tuple(p0)
+        assert tuple(res_edge["pointB"]) == tuple(p1)
+        assert math.isclose(res_edge["length"], math.dist(p0, p1))
+    else:
+        pytest.skip("not enough points for edge pick test")
+
diff --git a/tests/notebooks/test_notebook_VisorUsage.ipynb b/tests/notebooks/test_notebook_VisorUsage.ipynb
new file mode 100644
index 00000000..9c39304a
--- /dev/null
+++ b/tests/notebooks/test_notebook_VisorUsage.ipynb
@@ -0,0 +1,178 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e3d36795-601a-4f30-b0e2-9b290c7c5ce4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from IPython.display import IFrame\n",
+    "\n",
+    "from ansys.visor.viewer import Visor\n",
+    "\n",
+    "asset_path1 = '../../examples/assets/vtk_scene_sphere_l2_b3_r32_v3_c1_z0.vtm'\n",
+    "asset_path2 = '../../examples/assets/tensors9.vtp'\n",
+    "\n",
+    "#######################################################\n",
+    "# Example running Visor in a jupyter notebook\n",
+    "# Shows how to create two separate Visor visualizers\n",
+    "# on two distinct ports (8081, 8082) and\n",
+    "# view them in an iframe\n",
+    "#######################################################"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7ee1d84c-cb55-45e8-934b-fc23b78ef406",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Instantiate a Visor visualizer on port 8081\n",
+    "visor = Visor(url='http://127.0.0.1:8081')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8e5c72f6-0fed-46e7-8d1b-b628b01735f4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Start the visualizer using the first asset\n",
+    "dataset_id1 = visor.start(asset_path1)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "86371399-3e20-4990-a6c1-fb210868717e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# NBVAL_IGNORE_OUTPUT\n",
+    "# View the visualizer inline\n",
+    "IFrame(src='http://127.0.0.1:8081', width=1200, height=600)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "97d60b19",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset_id2 = visor.update(asset_path2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "4d84dc32-29bb-4882-9683-5d234e585913",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Print the information about the first visualizer instance\n",
+    "visor.info()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "524d1ff2-7a21-45e1-94b1-b4f29822df7c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# NBVAL_IGNORE_OUTPUT\n",
+    "# Initialize a second Visor visualizer on port 8082\n",
+    "visor1 = Visor(url='http://127.0.0.1:8082')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "dda46dc4-d2ff-4929-8efe-65fc43868a5d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset_id3 = visor1.start(asset_path2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "678a64ee-d78f-4c13-aed4-a73812220063",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# NBVAL_IGNORE_OUTPUT\n",
+    "IFrame(src='http://127.0.0.1:8082', width=1200, height=600)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1b80bd3d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import requests\n",
+    "\n",
+    "r = requests.get(\"http://localhost:8081/index.html\", timeout=5)\n",
+    "print(r.status_code, r.headers.get(\"content-type\"))\n",
+    "r = requests.get(\"http://localhost:8082/index.html\", timeout=5)\n",
+    "print(r.status_code, r.headers.get(\"content-type\"))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "836f1ce3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "visor.stop()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "dbfd3e25",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "visor1.stop()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0fd13268-0865-4eee-8c80-88d7d01f7838",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.12.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tests/pytest.ini b/tests/pytest.ini
new file mode 100644
index 00000000..03f64ddd
--- /dev/null
+++ b/tests/pytest.ini
@@ -0,0 +1,19 @@
+[pytest]
+
+markers =
+    smoke: mark a test as a smoke test
+    regression: mark a test as a regression test
+    integration: tests combining multiple components
+    unit: isolated tests
+    notebook: mark a test that runs a notebook
+    saf: SAF integration smoke tests (requires saf-visor-poc installed)
+
+norecursedirs =
+    e2e
+    regressions
+    smoke
+    files
+    logs
+    notebooks
+    reference_images
+    __pycache__
diff --git a/tests/references/dash_snapshot_reference.json b/tests/references/dash_snapshot_reference.json
new file mode 100644
index 00000000..569da5ce
--- /dev/null
+++ b/tests/references/dash_snapshot_reference.json
@@ -0,0 +1,169 @@
+{
+  "ui": {
+    "darkTheme": null
+  },
+  "scene": {
+    "unit": "m",
+    "spectrumStates": {
+      "POINT::Normals::3": {
+        "id": "POINT::Normals::3",
+        "magnitudeRange": [
+          0.9999999680464912,
+          1.0000000418852286
+        ],
+        "ranges": [
+          [
+            -0.9987165331840515,
+            0.9987165331840515
+          ],
+          [
+            -0.9987165331840515,
+            0.9987165331840515
+          ],
+          [
+            -1,
+            1
+          ]
+        ]
+      },
+      "POINT::gradient_variable_0::1": {
+        "id": "POINT::gradient_variable_0::1",
+        "magnitudeRange": [
+          0,
+          1
+        ],
+        "ranges": [
+          [
+            0,
+            1
+          ]
+        ]
+      },
+      "POINT::constant_variable_1::1": {
+        "id": "POINT::constant_variable_1::1",
+        "magnitudeRange": [
+          1,
+          1
+        ],
+        "ranges": [
+          [
+            1,
+            1
+          ]
+        ]
+      },
+      "POINT::random_variable_2::1": {
+        "id": "POINT::random_variable_2::1",
+        "magnitudeRange": [
+          2.041605330305174e-05,
+          0.9999546408653259
+        ],
+        "ranges": [
+          [
+            2.041605330305174e-05,
+            0.9999546408653259
+          ]
+        ]
+      }
+    },
+    "datasetStates": {
+      "vtk_scene_sphere_l2_b3_r32_v3_c1_z3": {
+        "id": "8179120606148486",
+        "keyedByName": true,
+        "name": "vtk_scene_sphere_l2_b3_r32_v3_c1_z3",
+        "partStates": {
+          "level_1_block_0_part_0": {
+            "id": "3721921905183888",
+            "keyedByName": true,
+            "name": "level_1_block_0_part_0",
+            "opacity": 0.1111111111111111,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_0_part_1": {
+            "id": "7704132612406141",
+            "keyedByName": true,
+            "name": "level_1_block_0_part_1",
+            "opacity": 0.1111111111111111,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_0_part_2": {
+            "id": "6038438258649815",
+            "keyedByName": true,
+            "name": "level_1_block_0_part_2",
+            "opacity": 0.1111111111111111,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_1_part_0": {
+            "id": "5212416988329659",
+            "keyedByName": true,
+            "name": "level_1_block_1_part_0",
+            "opacity": 0.2222222222222222,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_1_part_1": {
+            "id": "3795994468691737",
+            "keyedByName": true,
+            "name": "level_1_block_1_part_1",
+            "opacity": 0.2222222222222222,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_1_part_2": {
+            "id": "2633610373980222",
+            "keyedByName": true,
+            "name": "level_1_block_1_part_2",
+            "opacity": 0.2222222222222222,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_2_part_0": {
+            "id": "1366333556315428",
+            "keyedByName": true,
+            "name": "level_1_block_2_part_0",
+            "opacity": 0.3333333333333333,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_2_part_1": {
+            "id": "1809875816089870",
+            "keyedByName": true,
+            "name": "level_1_block_2_part_1",
+            "opacity": 0.3333333333333333,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          },
+          "level_1_block_2_part_2": {
+            "id": "4221596239203654",
+            "keyedByName": true,
+            "name": "level_1_block_2_part_2",
+            "opacity": 0.3333333333333333,
+            "visible": true,
+            "selected": false,
+            "spectrumId": "",
+            "spectrumComponent": -1
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/references/dash_start_baseline.png b/tests/references/dash_start_baseline.png
new file mode 100644
index 00000000..45691466
Binary files /dev/null and b/tests/references/dash_start_baseline.png differ
diff --git a/tests/references/e2e01_tensor_component_dropdown_baseline.png b/tests/references/e2e01_tensor_component_dropdown_baseline.png
new file mode 100644
index 00000000..38d0be14
Binary files /dev/null and b/tests/references/e2e01_tensor_component_dropdown_baseline.png differ
diff --git a/tests/references/e2e02_tensor_initial_render_baseline.png b/tests/references/e2e02_tensor_initial_render_baseline.png
new file mode 100644
index 00000000..40e86e1a
Binary files /dev/null and b/tests/references/e2e02_tensor_initial_render_baseline.png differ
diff --git a/tests/references/st03_canvas_initial_baseline.png b/tests/references/st03_canvas_initial_baseline.png
new file mode 100644
index 00000000..8caa22c9
Binary files /dev/null and b/tests/references/st03_canvas_initial_baseline.png differ
diff --git a/tests/references/st03_canvas_rotation_baseline.png b/tests/references/st03_canvas_rotation_baseline.png
new file mode 100644
index 00000000..1b50ffcd
Binary files /dev/null and b/tests/references/st03_canvas_rotation_baseline.png differ
diff --git a/tests/references/test_dash_canvas_baseline.png b/tests/references/test_dash_canvas_baseline.png
new file mode 100644
index 00000000..49ba445e
Binary files /dev/null and b/tests/references/test_dash_canvas_baseline.png differ
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/api/test_models.py b/tests/unit/api/test_models.py
new file mode 100644
index 00000000..9299abcb
--- /dev/null
+++ b/tests/unit/api/test_models.py
@@ -0,0 +1,89 @@
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.api.models import Info, InitProps, RemoveDatasetProps, StartProps, UpdateProps
+from ansys.visor.viewer.core.metadata import Metadata
+
+
+def test_info_model_valid():
+    """Verify that an Info model is created successfully with valid values."""
+    info = Info(
+        app_name="visor",
+        host="localhost",
+        port=8080,
+        standalone=True,
+        datasets=["file1.vtk", "file2.vtk"]
+    )
+    assert info.app_name == "visor"
+    assert info.host == "localhost"
+    assert info.port == 8080
+    assert info.standalone is True
+    assert info.datasets == ["file1.vtk", "file2.vtk"]
+
+def test_info_model_missing_fields():
+    """Verify that creating an Info model without required fields raises a ValidationError."""
+    with pytest.raises(ValidationError):
+        Info(host="localhost", port=8080, standalone=True, datasets=[])
+
+def test_initprops_valid_host():
+    """Test that initprops valid host."""
+    props = InitProps(host="localhost", port=8081)
+    assert props.host == "localhost"
+    assert props.port == 8081
+
+def test_initprops_port_defaults_to_zero():
+    """Port must default to 0 (sentinel for server-side selection)."""
+    props = InitProps(host="localhost")
+    assert props.port == 0
+
+def test_initprops_invalid_host():
+    """Verify that InitProps raises a ValidationError for an invalid host."""
+    with pytest.raises(ValidationError):
+        InitProps(host="bad host!", port=8081)
+
+def test_startprops_all_fields():
+    """Verify that StartProps stores all provided field values."""
+    meta = Metadata(name="foo", unit="m")
+    props = StartProps(file_path="file.vtk", metadata=meta, timeout=10)
+    assert props.file_path == "file.vtk"
+    assert props.metadata == meta
+    assert props.timeout == 10
+
+def test_startprops_metadata_as_str():
+    """Verify that StartProps accepts metadata as a string path."""
+    props = StartProps(file_path="file.vtk", metadata="meta.json", timeout=5)
+    assert props.metadata == "meta.json"
+
+def test_startprops_defaults():
+    """Verify that StartProps uses the expected default values when no arguments are provided."""
+    props = StartProps()
+    assert props.file_path is None
+    assert props.metadata is None
+    assert props.timeout == 0
+
+def test_updateprops_valid():
+    """Verify that UpdateProps is created successfully with valid values."""
+    meta = Metadata(name="bar", unit="cm")
+    props = UpdateProps(file_path="updated.vtk", metadata=meta)
+    assert props.file_path == "updated.vtk"
+    assert props.metadata == meta
+
+def test_updateprops_metadata_as_str():
+    """Verify that UpdateProps accepts metadata as a string path."""
+    props = UpdateProps(file_path="updated.vtk", metadata="meta.json")
+    assert props.metadata == "meta.json"
+
+def test_updateprops_missing_file_path():
+    """Verify that UpdateProps raises a ValidationError when file_path is missing."""
+    with pytest.raises(ValidationError):
+        UpdateProps(metadata="meta.json")
+
+def test_removedatasetprops_valid():
+    """Verify that RemoveDatasetProps accepts a valid dataset identifier."""
+    props = RemoveDatasetProps(dataset_id=12345)
+    assert props.dataset_id == 12345
+
+def test_removedatasetprops_invalid_type():
+    """Verify that RemoveDatasetProps raises a ValidationError for a non-integer dataset identifier."""
+    with pytest.raises(ValidationError):
+        RemoveDatasetProps(dataset_id="not_an_int")
diff --git a/tests/unit/api/test_server.py b/tests/unit/api/test_server.py
new file mode 100644
index 00000000..51513dcf
--- /dev/null
+++ b/tests/unit/api/test_server.py
@@ -0,0 +1,588 @@
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+import ansys.visor.viewer.api.server as server_mod
+
+
+@pytest.fixture
+def mock_settings():
+    """Provide mock application settings."""
+    settings = MagicMock()
+    settings.default_host = "localhost"
+    settings.default_port = 8081
+    settings.default_standalone = True
+    settings.trame_log_dir = "logdir"
+    settings.app_name = "TestApp"
+    settings.binding_host = None
+    # Ensure SSL-related attributes are explicit for tests
+    settings.ssl_certificate = None
+    settings.url_scheme = "http"
+    return settings
+
+
+@pytest.fixture
+def api(mock_settings):
+    """Provide a VisorAPI instance with mocked dependencies."""
+    with patch.object(server_mod, "settings", mock_settings), \
+         patch.object(server_mod, "VisorCache") as mock_cache, \
+         patch.object(server_mod, "VisorLogger"), \
+         patch.object(server_mod, "Visor"):
+        mock_cache.get_instance.return_value = (MagicMock(), None)
+        mock_cache.list_instances.return_value = ["http://localhost:8081"]
+        return server_mod.VisorAPI()
+
+
+def test_get_url(api):
+    """Verify that a URL is constructed from the host and port."""
+    url = api._get_url("host", 1234)
+    assert url == "http://host:1234"
+
+
+def test_server_get_url_reports_public_host_not_binding_host(monkeypatch, api):
+    """_get_url should report the public host passed to initialize, not the binding/listen host."""
+    # When binding_host is set, Trame will listen on it, but advertised URLs must use the public host
+    monkeypatch.setenv("GLOW_PRODUCT_BINDING_HOST", "example.com")
+    api.settings.binding_host = "example.com"
+    url = api._get_url("127.0.0.1", 8080)
+    assert url == "http://127.0.0.1:8080"
+
+    # When binding_host is not set, behaviour is unchanged
+    monkeypatch.delenv("GLOW_PRODUCT_BINDING_HOST", raising=False)
+    api.settings.binding_host = None
+    url2 = api._get_url("127.0.0.1", 8080)
+    assert url2 == "http://127.0.0.1:8080"
+
+
+def test_initialize_visualizer(api):
+    """Verify that an existing visualizer instance is returned from the cache."""
+    with patch.object(server_mod, "VisorCache") as mock_cache:
+        mock_instance = MagicMock()
+        mock_cache.get_instance.return_value = (mock_instance, None)
+        result, warnings = api._connect_or_initialize_visualizer("host", 1234, True, False)
+        assert result is mock_instance
+        assert warnings is None
+        mock_cache.get_instance.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_info_with_visualizer(api):
+    """Verify that visualizer information is returned when available."""
+    api.visualizer = MagicMock()
+    api.visualizer.info.return_value = {
+        "app_name": "TestApp",
+        "host": "h",
+        "port": 1,
+        "standalone": True,
+        "datasets": [],
+        "metadata": None
+    }
+    result = await api.info()
+    assert result.app_name == "TestApp"
+
+
+@pytest.mark.asyncio
+async def test_info_without_visualizer(api, mock_settings):
+    """Verify that default information is returned when no visualizer exists."""
+    api.visualizer = None
+    # Ensure all required fields are present
+    result = await api.info()
+    assert result.app_name == mock_settings.app_name
+    assert result.host == mock_settings.default_host
+    assert result.port == mock_settings.default_port
+    assert result.standalone == mock_settings.default_standalone
+    assert isinstance(result.datasets, list)
+
+
+@pytest.mark.asyncio
+async def test_list_instances(api):
+    """Verify that available instance URLs are returned."""
+    result = await api.list_instances()
+    assert "urls" in result
+    assert isinstance(result["urls"], list)
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_decorator_catches_known_errors(api):
+    """Verify that known errors are converted to HTTP 422 responses."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise server_mod.errors.InvalidUrlError("bad")
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await dummy(api)
+
+    assert exc.value.status_code == 422
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_decorator_catches_runtime_error(api):
+    """Verify that runtime errors are converted to HTTP 500 responses."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise RuntimeError("fail")
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await dummy(api)
+
+    assert exc.value.status_code == 500
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_decorator_catches_unhandled(api):
+    """Verify that unhandled exceptions are converted to HTTP 500 responses."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise Exception("fail")
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await dummy(api)
+
+    assert exc.value.status_code == 500
+
+
+@pytest.mark.asyncio
+async def test_initialize_server(api):
+    """Verify that a server instance can be initialized successfully."""
+    props = MagicMock()
+    props.host = "h"
+    props.port = 1
+    props.standalone = True
+    props.dark_mode = False
+
+    with patch.object(api, "_connect_or_initialize_visualizer", return_value=(MagicMock(), None)):
+        result = await api.connect_or_initialize_server(props)
+        assert "Set active instance" in result["message"]
+        assert result["message"] == "Set active instance to http://h:1"
+
+
+@pytest.mark.asyncio
+async def test_initialize_server_port_zero_uses_find_unused_port(api):
+    """When port is 0, the server must call find_unused_port and use its result."""
+    props = MagicMock()
+    props.host = "h"
+    props.port = 0
+    props.standalone = True
+    props.dark_mode = False
+
+    with patch.object(api, "_connect_or_initialize_visualizer",
+                      return_value=(MagicMock(), None)) as mock_conn, \
+         patch.object(server_mod, "find_unused_port", return_value=55555) as mock_find:
+        result = await api.connect_or_initialize_server(props)
+
+    # find_unused_port must have been asked for a port on the request host
+    mock_find.assert_called_once_with(host="h")
+    # The picked port must be forwarded to the visualizer factory
+    call_args = mock_conn.call_args
+    assert call_args.args[0] == "h"
+    assert call_args.args[1] == 55555
+    # And reflected in the response message
+    assert result["message"] == "Set active instance to http://h:55555"
+
+
+@pytest.mark.asyncio
+async def test_initialize_server_port_zero_probes_binding_host_when_set(api):
+    """When settings.binding_host is set, that must be used as the probe host."""
+    api.settings.binding_host = "0.0.0.0"
+
+    props = MagicMock()
+    props.host = "h"
+    props.port = 0
+    props.standalone = True
+    props.dark_mode = False
+
+    with patch.object(api, "_connect_or_initialize_visualizer",
+                      return_value=(MagicMock(), None)), \
+         patch.object(server_mod, "find_unused_port", return_value=44444) as mock_find:
+        await api.connect_or_initialize_server(props)
+
+    mock_find.assert_called_once_with(host="0.0.0.0")
+
+
+@pytest.mark.asyncio
+async def test_initialize_server_port_zero_no_free_port_raises_503(api):
+    """When find_unused_port returns None, the endpoint must raise HTTP 503."""
+    props = MagicMock()
+    props.host = "h"
+    props.port = 0
+    props.standalone = True
+    props.dark_mode = False
+
+    with patch.object(server_mod, "find_unused_port", return_value=None):
+        with pytest.raises(server_mod.HTTPException) as exc:
+            await api.connect_or_initialize_server(props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_initialize_server_forwards_warnings(api):
+    """Warnings returned by _connect_or_initialize_visualizer must reach the response."""
+    props = MagicMock()
+    props.host = "h"
+    props.port = 1
+    props.standalone = True
+    props.dark_mode = False
+
+    warnings = ["ignored standalone flag"]
+    with patch.object(api, "_connect_or_initialize_visualizer",
+                      return_value=(MagicMock(), warnings)):
+        result = await api.connect_or_initialize_server(props)
+
+    assert result["warnings"] == warnings
+
+
+@pytest.mark.asyncio
+async def test_start_instance_success(api):
+    """Verify that a visualization can be started successfully."""
+    api.visualizer = MagicMock()
+    api.visualizer._start_async = AsyncMock(return_value=("dataset_id", "task"))
+
+    props = MagicMock()
+    props.file_path = "file"
+    props.metadata = {"name": "m"}
+    props.timeout = 0
+
+    result = await api.start_instance(props)
+    assert "success" in result
+
+
+@pytest.mark.asyncio
+async def test_start_instance_no_visualizer(api):
+    """Verify that starting a visualization requires an active visualizer."""
+    api.visualizer = None
+    props = MagicMock()
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.start_instance(props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_update_success(api):
+    """Verify that an active visualization can be updated."""
+    api.visualizer = MagicMock()
+    api.visualizer.update = MagicMock(return_value=None)
+
+    props = MagicMock()
+    props.file_path = "file"
+    props.metadata = {"name": "m"}
+
+    result = await api.update(props)
+    assert "success" in result
+
+
+@pytest.mark.asyncio
+async def test_update_no_visualizer(api):
+    """Verify that updating requires an active visualizer."""
+    api.visualizer = None
+    props = MagicMock()
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.update(props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_stop_visualization_success(api):
+    """Verify that an active visualization can be stopped."""
+    api.visualizer = MagicMock()
+    api.visualizer.url = "url"
+    api.visualizer._stop_async = AsyncMock(return_value=None)
+
+    result = await api.stop_visualization()
+    assert "success" in result
+
+
+@pytest.mark.asyncio
+async def test_stop_instance_success(api):
+    """Verify that an active instance can be stopped and cleared."""
+    api.visualizer = MagicMock()
+    api.visualizer.url = "url"
+    api.visualizer._stop_async = AsyncMock(return_value=None)
+
+    with patch.object(server_mod, "VisorCache"):
+        result = await api.stop_instance()
+        assert "success" in result
+        assert api.visualizer is None
+
+
+@pytest.mark.asyncio
+async def test_add_dataset_success(api):
+    """Verify that a dataset can be added successfully."""
+    api.visualizer = MagicMock()
+    api.visualizer.add_dataset = MagicMock(return_value=None)
+
+    props = MagicMock()
+    props.file_path = "file"
+    props.metadata = {"name": "m"}
+
+    result = await api.add_dataset(props)
+    assert "success" in result
+
+
+@pytest.mark.asyncio
+async def test_add_dataset_no_visualizer(api):
+    """Verify that adding a dataset requires an active visualizer."""
+    api.visualizer = None
+    props = MagicMock()
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.add_dataset(props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_remove_dataset_success(api):
+    """Verify that a dataset can be removed successfully."""
+    api.visualizer = MagicMock()
+    api.visualizer.url = "url"
+    api.visualizer.remove_dataset = MagicMock(return_value=None)
+
+    props = MagicMock()
+    props.dataset_id = 123
+
+    result = await api.remove_dataset(props)
+    assert "success" in result
+
+
+@pytest.mark.asyncio
+async def test_remove_dataset_no_visualizer(api):
+    """Verify that removing a dataset requires an active visualizer."""
+    api.visualizer = None
+    props = MagicMock()
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.remove_dataset(props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_list_datasets_success(api):
+    """Verify that datasets are returned from the visualizer."""
+    api.visualizer = MagicMock()
+    api.visualizer.list_datasets = MagicMock(return_value={"a": 1})
+
+    result = await api.list_datasets()
+    assert "datasets" in result
+
+
+@pytest.mark.asyncio
+async def test_list_datasets_no_visualizer(api):
+    """Verify that listing datasets requires an active visualizer."""
+    api.visualizer = None
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.list_datasets()
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_health_live(api):
+    """Verify that the health endpoint reports an OK status."""
+    result = await api.health_live()
+    assert result["status"] == "ok"
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_add_dataset_error(api):
+    """Verify that dataset addition errors return a sentinel response."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise server_mod.errors.VisorAddDatasetError("fail")
+
+    result = await dummy(api)
+
+    assert result["success"] is False
+    assert result["dataset_id"] == -1
+    assert "Failed to add dataset" in result["error"]
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_invalid_file(api):
+    """Verify that InvalidFileError is converted to an HTTP 422 response."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise server_mod.errors.InvalidFileError("bad")
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await dummy(api)
+
+    assert exc.value.status_code == 422
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_invalid_file_format(api):
+    """Verify that InvalidFileFormatError is converted to an HTTP 422 response."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise server_mod.errors.InvalidFileFormatError("bad")
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await dummy(api)
+
+    assert exc.value.status_code == 422
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_server_not_started(api):
+    """Verify that ServerNotStartedError is converted to an HTTP 400 response."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise server_mod.errors.ServerNotStartedError("bad")
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await dummy(api)
+
+    assert exc.value.status_code == 400
+
+
+@pytest.mark.asyncio
+async def test_handle_exceptions_type_error(api):
+    """Verify that TypeError is converted to an HTTP 500 response."""
+
+    @server_mod.handle_exceptions
+    async def dummy(self):
+        raise TypeError("bad")
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await dummy(api)
+
+    assert exc.value.status_code == 500
+
+
+@pytest.mark.asyncio
+async def test_list_variables_no_visualizer(api):
+    """Verify that listing variables requires an active visualizer."""
+
+    api.visualizer = None
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.list_variables(1)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_update_variables_no_visualizer(api):
+    """Verify that updating variables requires an active visualizer."""
+
+    api.visualizer = None
+
+    props = MagicMock()
+    props.variables = []
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.update_variables(1, props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_save_state_no_visualizer(api):
+    """Verify that saving state requires an active visualizer."""
+
+    api.visualizer = None
+
+    props = MagicMock()
+    props.state_dir = "dir"
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.save_state(props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_load_state_no_visualizer(api):
+    """Verify that loading state requires an active visualizer."""
+
+    api.visualizer = None
+
+    props = MagicMock()
+    props.state_dir = "dir"
+
+    with pytest.raises(server_mod.HTTPException) as exc:
+        await api.load_state(props)
+
+    assert exc.value.status_code == 503
+
+
+@pytest.mark.asyncio
+async def test_list_variables_success(api):
+    """Verify that variable results are serialized using to_dict."""
+
+    part = MagicMock()
+    part.to_dict.return_value = {"id": 1}
+
+    api.visualizer = MagicMock()
+    api.visualizer.list_variables.return_value = [part]
+
+    result = await api.list_variables(1)
+
+    assert result == {"parts": [{"id": 1}]}
+
+
+@pytest.mark.asyncio
+async def test_update_variables_success(api):
+    """Verify that variables are serialized before being forwarded."""
+
+    var = MagicMock()
+    var.model_dump.return_value = {"x": 1}
+
+    props = MagicMock()
+    props.variables = [var]
+
+    api.visualizer = MagicMock()
+    api.visualizer.update_variables.return_value = {"ok": True}
+
+    result = await api.update_variables(5, props)
+
+    assert result == {"variables": {"ok": True}}
+    api.visualizer.update_variables.assert_called_once_with(5, [{"x": 1}])
+
+
+@pytest.mark.asyncio
+async def test_save_state_success(api):
+    """Verify that state can be saved successfully."""
+
+    api.visualizer = MagicMock()
+    api.visualizer.url = "url"
+    api.visualizer.save_state = AsyncMock()
+
+    props = MagicMock()
+    props.state_dir = "dir"
+
+    result = await api.save_state(props)
+
+    assert "Saved state to dir" in result["success"]
+    api.visualizer.save_state.assert_called_once_with("dir")
+
+
+@pytest.mark.asyncio
+async def test_load_state_success(api):
+    """Verify that state can be loaded successfully."""
+
+    api.visualizer = MagicMock()
+    api.visualizer.url = "url"
+    api.visualizer.load_state = MagicMock()
+
+    props = MagicMock()
+    props.state_dir = "dir"
+
+    result = await api.load_state(props)
+
+    assert "Loaded state from dir" in result["success"]
+    api.visualizer.load_state.assert_called_once_with("dir")
diff --git a/tests/unit/api/test_visor_cache.py b/tests/unit/api/test_visor_cache.py
new file mode 100644
index 00000000..bfb52cc2
--- /dev/null
+++ b/tests/unit/api/test_visor_cache.py
@@ -0,0 +1,81 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer.api.visor_cache import VisorCache
+from ansys.visor.viewer.core.visor_enums import RenderingMode
+
+
+@pytest.fixture(autouse=True)
+def clear_cache():
+    """Clear the instance cache before each test."""
+    # Ensure cache is empty before each test
+    VisorCache._instances.clear()
+
+
+def test_get_instance_creates_new(monkeypatch):
+    """Verify that a new instance is created when none exists."""
+    mock_visor = MagicMock()
+    with patch("ansys.visor.viewer.api.visor_cache.Visor", return_value=mock_visor) as mock_cls:
+        inst, warnings = VisorCache.get_instance(
+            "url1",
+            rendering_mode=RenderingMode.LOCAL,
+            standalone=False,
+            dark_mode=False,
+            trame_log_dir="dir"
+        )
+        assert inst is mock_visor
+        assert warnings is None
+        assert "url1" in VisorCache._instances
+        mock_cls.assert_called_once_with(
+            url="url1",
+            rendering_mode=RenderingMode.LOCAL,
+            standalone=False,
+            dark_mode=False,
+            trame_log_dir="dir"
+        )
+
+
+def test_get_instance_returns_existing(monkeypatch):
+    """Verify that an existing instance is returned from the cache."""
+    mock_visor = MagicMock()
+    mock_visor.standalone = False
+    mock_visor.dark_mode = False
+    VisorCache._instances["url2"] = mock_visor
+
+    with patch("ansys.visor.viewer.api.visor_cache.Visor") as mock_cls:
+        inst, warnings = VisorCache.get_instance(
+            "url2",
+            rendering_mode=RenderingMode.LOCAL,
+            standalone=False,
+            dark_mode=False
+        )
+        assert inst is mock_visor
+        mock_cls.assert_not_called()
+
+
+def test_delete_instance_removes(monkeypatch):
+    """Verify that an instance is removed from the cache."""
+    mock_visor = MagicMock()
+    VisorCache._instances["url3"] = mock_visor
+
+    VisorCache.delete_instance("url3")
+
+    assert "url3" not in VisorCache._instances
+
+
+def test_delete_instance_noop_if_missing(monkeypatch):
+    """Verify that deleting a missing instance has no effect."""
+    # Should not raise or change anything
+    VisorCache.delete_instance("missing_url")
+    assert VisorCache._instances == {}
+
+
+def test_list_instances_returns_keys(monkeypatch):
+    """Verify that cached instance URLs are returned."""
+    VisorCache._instances["urlA"] = MagicMock()
+    VisorCache._instances["urlB"] = MagicMock()
+
+    urls = VisorCache.list_instances()
+
+    assert set(urls) == {"urlA", "urlB"}
diff --git a/tests/unit/app/test_async_runner.py b/tests/unit/app/test_async_runner.py
new file mode 100644
index 00000000..25f15775
--- /dev/null
+++ b/tests/unit/app/test_async_runner.py
@@ -0,0 +1,94 @@
+import asyncio
+import threading
+from concurrent.futures import Future as ConcurrentFuture
+
+import pytest
+
+from ansys.visor.viewer.app.trame.async_runner import AsyncRunner
+
+
+@pytest.fixture
+def runner():
+    """Provide an AsyncRunner instance."""
+    r = AsyncRunner()
+    yield r
+    r.dispose()
+
+def test_thread_and_loop_created_once(runner):
+    """Verify that the thread instance is created only once."""
+    thread1 = runner.thread
+    thread2 = runner.thread
+    assert thread1 is thread2
+    assert isinstance(thread1, threading.Thread)
+    assert thread1.is_alive()
+
+@pytest.mark.asyncio
+async def test_run_coroutine_async_simple(runner):
+    """Verify that a coroutine can be executed asynchronously."""
+    async def coro():
+        await asyncio.sleep(0.01)
+        return 42
+    result = await runner.run_coroutine_async(coro(), patch_return_future=False)
+    assert result == 42
+
+def test_run_coroutine_block_simple(runner):
+    """Verify that a coroutine can be executed synchronously."""
+    async def coro():
+        await asyncio.sleep(0.01)
+        return "done"
+    result = runner.run_coroutine_block(coro(), patch_return_future=False)
+    assert result == "done"
+
+def test_run_coroutine_returns_concurrent_future(runner):
+    """Verify that run_coroutine returns a concurrent future."""
+    async def coro():
+        return "future"
+    fut = runner.run_coroutine(coro(), patch_return_future=False)
+    assert isinstance(fut, ConcurrentFuture)
+    assert fut.result() == "future"
+
+def test_run_coroutine_block_exception(runner):
+    """Verify that synchronous execution propagates coroutine exceptions."""
+    async def coro():
+        raise ValueError("fail")
+    with pytest.raises(ValueError, match="fail"):
+        runner.run_coroutine_block(coro(), patch_return_future=False)
+
+@pytest.mark.asyncio
+async def test_run_coroutine_async_exception(runner):
+    """Verify that asynchronous execution propagates coroutine exceptions."""
+    async def coro():
+        raise RuntimeError("async fail")
+    with pytest.raises(RuntimeError, match="async fail"):
+        await runner.run_coroutine_async(coro(), patch_return_future=False)
+
+def test_patch_return_future(runner):
+    """Verify that returned futures can be patched into awaitable coroutines."""
+    async def inner():
+        await asyncio.sleep(0.01)
+        return 123
+    async def outer():
+        fut = asyncio.ensure_future(inner())
+        return fut
+    # patch_return_future=True returns a coroutine
+    patched = runner.run_coroutine(outer(), patch_return_future=True).result()
+    # patched is a coroutine, can be awaited
+    loop = asyncio.new_event_loop()
+    result = loop.run_until_complete(patched)
+    assert result == 123
+    loop.close()
+
+def test_dispose_stops_and_closes_loop(runner):
+    """Verify that disposal stops and closes the event loop."""
+    # Should not raise
+    runner.dispose()
+    # Dispose again is a no-op
+    runner.dispose()
+
+def test_thread_is_daemon(runner):
+    """Verify that the runner thread is configured as a daemon."""
+    assert runner.thread.daemon
+
+def test_thread_is_alive_after_creation(runner):
+    """Verify that the runner thread is alive after creation."""
+    assert runner.thread.is_alive()
diff --git a/tests/unit/app/test_thiea.py b/tests/unit/app/test_thiea.py
new file mode 100644
index 00000000..e5292900
--- /dev/null
+++ b/tests/unit/app/test_thiea.py
@@ -0,0 +1,185 @@
+import pytest
+
+from ansys.visor.viewer.app.visor import Visor
+
+
+class ConcreteVisor(Visor):
+    def start(self, input=None, metadata=None, timeout=0, on_server_started=None):
+        self.started = True
+
+    def stop(self):
+        self.stopped = True
+
+    def update(self, input, metadata=None):
+        self.updated = True
+
+    def save_state(self, state_dir):
+        self.state_saved = state_dir
+
+    def load_state(self, state_dir):
+        self.state_loaded = state_dir
+
+    def _health(self):
+        return True
+
+    async def _start_async(self, input=None, metadata=None, timeout=0):
+        self.async_started = True
+
+    async def _stop_async(self):
+        self.async_stopped = True
+
+    def add_dataset(self, input, metadata=None):
+        self.dataset_added = True
+
+    def remove_dataset(self, dataset_id):
+        self.dataset_removed = dataset_id
+
+    def list_datasets(self):
+        return {
+            1: {
+                "id": 1,
+                "name": "foo",
+                "unit": "m",
+                "file_path": None,
+                "metadata_path": None,
+            }
+        }
+
+def test_factory_returns_subclass():
+    """Verify that the Visor factory returns a concrete implementation."""
+    obj = Visor(url="http://localhost:1234")
+    from ansys.visor.viewer.app.visor_vtk import VisorVTK
+    assert isinstance(obj, VisorVTK)
+
+def test_invalid_rendering_engine_raises():
+    """Verify that an invalid rendering engine raises a ValueError."""
+    class DummyEngine:
+        pass
+    with pytest.raises(ValueError):
+        Visor(rendering_engine=DummyEngine)
+
+def test_set_default_url_sets_properties(monkeypatch):
+    """Verify that default settings are used to construct the URL."""
+    monkeypatch.setattr("ansys.visor.viewer.config.settings.default_host", "localhost")
+    monkeypatch.setattr("ansys.visor.viewer.config.settings.default_port", 1234)
+    monkeypatch.setattr("ansys.visor.viewer.config.settings.app_name", "TestApp")
+    obj = ConcreteVisor()
+    assert obj.host == "localhost"
+    assert obj.port == 1234
+    assert obj.url == "http://localhost:1234"
+
+def test_set_url_sets_properties():
+    """Verify that host, port, and URL are parsed from the provided URL."""
+    obj = ConcreteVisor(url="http://127.0.0.1:8080")
+    assert obj.host == "127.0.0.1"
+    assert obj.port == 8080
+    assert obj.url == "http://127.0.0.1:8080"
+
+def test_info_returns_expected_dict(monkeypatch):
+    """Verify that info returns the expected viewer metadata."""
+    monkeypatch.setattr("ansys.visor.viewer.config.settings.app_name", "TestApp")
+    obj = ConcreteVisor()
+    info = obj.info()
+    assert info["app_name"] == "TestApp"
+    assert info["host"] == obj.host
+    assert info["port"] == obj.port
+    assert info["standalone"] == obj.standalone
+    assert info["datasets"] == ["foo"]
+
+def test_standalone_property():
+    """Verify that the standalone property reflects the configured value."""
+    obj = ConcreteVisor(standalone=False)
+    assert obj.standalone is False
+
+def test_abstract_methods_raise():
+    """Verify that unimplemented abstract methods raise NotImplementedError."""
+    class DummyVisor(Visor):
+        @property
+        def datasets(self):
+            raise NotImplementedError()
+
+        def start(self, *a, **kw):
+            raise NotImplementedError()
+
+        def stop(self):
+            raise NotImplementedError()
+
+        def update(self, *a, **kw):
+            raise NotImplementedError()
+
+        def save_state(self, state_dir):
+            raise NotImplementedError()
+
+        def load_state(self, state_dir):
+            raise NotImplementedError()
+
+        def _health(self):
+            raise NotImplementedError()
+
+        async def _start_async(self, *a, **kw):
+            raise NotImplementedError()
+
+        async def _stop_async(self):
+            raise NotImplementedError()
+
+        def add_dataset(self, *a, **kw):
+            raise NotImplementedError()
+
+        def remove_dataset(self, dataset_id):
+            raise NotImplementedError()
+
+        def list_datasets(self):
+            raise NotImplementedError()
+
+    dummy = DummyVisor()
+    with pytest.raises(NotImplementedError):
+        _ = dummy.datasets
+    with pytest.raises(NotImplementedError):
+        dummy.start()
+    with pytest.raises(NotImplementedError):
+        dummy.stop()
+    with pytest.raises(NotImplementedError):
+        dummy.update("input")
+    with pytest.raises(NotImplementedError):
+        dummy.save_state("dir")
+    with pytest.raises(NotImplementedError):
+        dummy.load_state("dir")
+    with pytest.raises(NotImplementedError):
+        dummy._health()
+    with pytest.raises(NotImplementedError):
+        import asyncio
+        asyncio.run(dummy._start_async())
+    with pytest.raises(NotImplementedError):
+        import asyncio
+        asyncio.run(dummy._stop_async())
+    with pytest.raises(NotImplementedError):
+        dummy.add_dataset("input")
+    with pytest.raises(NotImplementedError):
+        dummy.remove_dataset(1)
+    with pytest.raises(NotImplementedError):
+        dummy.list_datasets()
+
+def test_concrete_methods_work():
+    """Verify that the concrete implementation methods update state as expected."""
+    obj = ConcreteVisor()
+    obj.start()
+    assert obj.started
+    obj.stop()
+    assert obj.stopped
+    obj.update("input")
+    assert obj.updated
+    obj.save_state("dir")
+    assert obj.state_saved == "dir"
+    obj.load_state("dir")
+    assert obj.state_loaded == "dir"
+    assert obj._health() is True
+    import asyncio
+    asyncio.run(obj._start_async())
+    assert obj.async_started
+    asyncio.run(obj._stop_async())
+    assert obj.async_stopped
+    obj.add_dataset("input")
+    assert obj.dataset_added
+    obj.remove_dataset(42)
+    assert obj.dataset_removed == 42
+    assert list(obj.list_datasets().keys()) == [1]
diff --git a/tests/unit/app/test_trame_server_manager.py b/tests/unit/app/test_trame_server_manager.py
new file mode 100644
index 00000000..1f58f1c3
--- /dev/null
+++ b/tests/unit/app/test_trame_server_manager.py
@@ -0,0 +1,218 @@
+import types
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer.app.trame.trame_server_manager import TrameServerManager
+from ansys.visor.viewer.models.runtime.visor_scene_details import VisorSceneDetails
+from ansys.visor.viewer.models.runtime.vtk.runtime_vtk_info import RuntimeVTKInfo
+
+visor_vtk_pipeline_state = VisorSceneDetails(
+        vtk_info=RuntimeVTKInfo(
+            scene_graph=None,
+        ),
+    )
+
+class DummyServer:
+    def __init__(self, running=True, controller=True):
+        self.running = running
+        self.state = MagicMock()
+        self.state.to_dict.return_value = {"foo": "bar"}
+        self.controller = MagicMock() if controller else None
+        self.start = MagicMock(return_value=AsyncMock())
+        self.stop = AsyncMock()
+        self.ready = AsyncMock()
+        self.http_headers = MagicMock()
+        self.http_headers.set_header = MagicMock()
+
+def make_manager(server=None):
+    mgr = TrameServerManager()
+    mgr._server = server or DummyServer()
+    mgr._async_runner = MagicMock()
+    return mgr
+
+def make_manager_with_fixed_wasm_ids():
+    mock_local = MagicMock()
+
+    # return stable IDs used by your expected `visor_state`
+    mock_local.get_wasm_id.return_value = 1
+    mock_local.register_vtk_object.return_value = 1
+    mock_local.ref_name = "wasm_ref"
+
+    # Patch the LocalView class imported in VtkPipeline so VtkPipeline() gets the mock
+    return patch(
+        "ansys.visor.viewer.vtk.vtk_pipeline.LocalView",
+        return_value=mock_local
+    )
+
+def test_init_sets_properties():
+    """Verify that initialization stores the configured host and port."""
+    mgr = TrameServerManager(host="h", port=123)
+    assert mgr._host == "h"
+    assert mgr._port == 123
+    assert mgr._server is not None
+    assert mgr._async_runner is not None
+
+def test_running_property_true():
+    """Verify that running returns True when the server is running."""
+    mgr = make_manager(DummyServer(running=True))
+    assert mgr.running is True
+
+def test_running_property_false():
+    """Verify that running returns False when the server is not running."""
+    mgr = make_manager(DummyServer(running=False))
+    assert mgr.running is False
+
+def test_url_property():
+    """Verify that the URL property is constructed from the host and port."""
+    mgr = TrameServerManager(host="abc", port=1234)
+    assert mgr.url == "http://abc:1234"
+
+def test_health_property_true():
+    """Verify that health reports True when the server is healthy."""
+    mgr = make_manager(DummyServer(running=True))
+    assert mgr.health is True
+
+def test_health_no_server_raises():
+    """Verify that health raises when no server is available."""
+    mgr = make_manager()
+    mgr._server = None
+    with pytest.raises(RuntimeError):
+        _ = mgr.health
+
+def test_health_no_controller_raises():
+    """Verify that health raises when the server controller is unavailable."""
+    mgr = make_manager(DummyServer())
+    mgr._server.controller = None
+    with pytest.raises(RuntimeError):
+        _ = mgr.health
+
+def test_start_calls_async_runner():
+    """Verify that start delegates execution to the async runner."""
+    mgr = make_manager()
+    mgr._async_runner.run_coroutine_block = MagicMock()
+    mgr.start(timeout=1, blocking=True)
+    mgr._async_runner.run_coroutine_block.assert_called_once()
+
+def test_stop_calls_async_runner():
+    """Verify that stop delegates execution to the async runner."""
+    mgr = make_manager()
+    mgr._async_runner.run_coroutine_block = MagicMock()
+    mgr.stop()
+    mgr._async_runner.run_coroutine_block.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_start_async_calls_async_runner():
+    """Verify that start_async delegates execution to the async runner."""
+    mgr = make_manager()
+    mgr._async_runner.run_coroutine_async = AsyncMock()
+    await mgr.start_async(timeout=1, blocking=True)
+    mgr._async_runner.run_coroutine_async.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_stop_async_calls_async_runner():
+    """Verify that stop_async delegates execution to the async runner."""
+    mgr = make_manager()
+    mgr._async_runner.run_coroutine_async = AsyncMock()
+    await mgr.stop_async()
+    mgr._async_runner.run_coroutine_async.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_state_property_raises():
+    """Verify that the base state property raises NotImplementedError."""
+    mgr = make_manager()
+    with pytest.raises(NotImplementedError):
+        _ = mgr.state
+
+def test_initialize_server_called(monkeypatch):
+    """Verify that server initialization uses the expected configuration."""
+    called = {}
+
+    def fake_get_server(name, log_network):
+        called["name"] = name
+        called["log_network"] = log_network
+        return MagicMock()
+
+    monkeypatch.setattr("ansys.visor.viewer.app.trame.trame_server_manager.get_server", fake_get_server)
+    monkeypatch.setattr("ansys.visor.viewer.app.trame.trame_server_manager.get_trame_log_path", lambda x: "log_path")
+
+    TrameServerManager(host="h", port=123, trame_log_dir="dir")
+
+    assert called["name"] == "http://h:123"
+    assert called["log_network"] == "log_path"
+
+@pytest.mark.asyncio
+async def test_start_core_invalid_timeout():
+    """Verify that _start_core rejects invalid timeout values."""
+    mgr = make_manager()
+    with pytest.raises(Exception):
+        await mgr._start_core(timeout=-1)
+
+@pytest.mark.asyncio
+async def test_start_core_server_coroutine_none():
+    """Verify that _start_core raises when server startup returns no coroutine."""
+    mgr = make_manager()
+    mgr._server.start = MagicMock(return_value=None)
+    with pytest.raises(RuntimeError):
+        await mgr._start_core()
+
+@pytest.mark.asyncio
+async def test_stop_core_running():
+    """Verify that _stop_core stops the server when it is running."""
+    mgr = make_manager()
+    mgr._server.running = True
+    mgr._server.stop = AsyncMock()
+    await mgr._stop_core()
+    mgr._server.stop.assert_awaited_once()
+
+@pytest.mark.asyncio
+async def test_stop_core_not_running():
+    """Verify that _stop_core does not stop an already stopped server."""
+    mgr = make_manager()
+    mgr._server.running = False
+    mgr._server.stop = AsyncMock()
+    await mgr._stop_core()
+    mgr._server.stop.assert_not_awaited()
+
+def test_trame_server_manager_configures_ssl_from_glow_certs_dir(tmp_path, monkeypatch):
+    """TrameServerManager should configure trame/wslink SSL when certs exist."""
+    # Create fake cert files
+    cert = tmp_path / "server.crt"
+    key = tmp_path / "server.key"
+    cert.write_text("CERT")
+    key.write_text("KEY")
+
+    # Prepare a fake server with a cli.set_defaults that captures kwargs
+    captured = {}
+
+    class FakeCLI:
+        def set_defaults(self, **kwargs):
+            captured.update(kwargs)
+
+    fake_server = types.SimpleNamespace(cli=FakeCLI())
+
+    # Ensure Settings exposes the resolved ssl_certificate
+    monkeypatch.setattr(
+        "ansys.visor.viewer.config.settings.ssl_certificate",
+        f"{str(cert)},{str(key)}",
+        raising=False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.app.trame.trame_server_manager.get_server",
+        lambda name, log_network: fake_server,
+    )
+
+    # Import here to ensure monkeypatch above applies before instantiation
+    from ansys.visor.viewer.app.trame.trame_server_manager import TrameServerManager
+
+    # Instantiate manager which will call _initialize_server and thus set_defaults
+    mgr = TrameServerManager(host="localhost", port=12345, trame_log_dir=None)
+
+    # Ensure our fake server was used
+    assert mgr.server is fake_server
+
+    # Verify set_defaults was called with an ssl value containing both paths
+    assert "ssl" in captured
+    ssl_val = captured["ssl"]
+    assert str(cert) in ssl_val
+    assert str(key) in ssl_val
\ No newline at end of file
diff --git a/tests/unit/app/test_visor_trame_app.py b/tests/unit/app/test_visor_trame_app.py
new file mode 100644
index 00000000..6eab74ef
--- /dev/null
+++ b/tests/unit/app/test_visor_trame_app.py
@@ -0,0 +1,203 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer.app.trame.local_app import LocalApp
+
+
+class MockController:
+    def __init__(self):
+        self.handlers = {}
+        self.add_call_count = 0
+    def add(self, event):
+        self.add_call_count += 1
+        def decorator(fn):
+            self.handlers[event] = fn
+            return fn
+        return decorator
+
+@pytest.fixture
+def mock_server():
+    """Provide a mock Trame server."""
+    server = MagicMock()
+    server.controller = MockController()
+    server.http_headers.set_header = MagicMock()
+    server.name = "TestServer"
+    server._www = None
+    return server
+
+
+@pytest.fixture
+def mock_logger():
+    """Provide a mock logger."""
+    logger = MagicMock()
+    logger.debug = MagicMock()
+    return logger
+
+
+@pytest.fixture
+def mock_get_scene_details_json():
+    """Provide a mock scene details callback."""
+    return MagicMock(return_value={"scene": "details"})
+
+
+@pytest.fixture
+def mock_handle_save_state_response():
+    """Provide a mock save-state callback."""
+    return MagicMock(return_value={"ok": True})
+
+
+@patch("ansys.visor.viewer.app.trame.local_app.settings")
+def test_init_sets_attributes(
+    mock_settings,
+    mock_server,
+    mock_get_scene_details_json,
+    mock_handle_save_state_response,
+    mock_logger,
+):
+    """Verify that initialization stores the provided attributes."""
+    mock_settings.get_client_bundle.return_value = "/mock/path"
+    app = LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+        trame_logger=mock_logger,
+    )
+    assert app.server == mock_server
+    assert app._get_scene_details_json == mock_get_scene_details_json
+    assert app._LocalApp__trame_logger == mock_logger
+
+
+def test_register_lifecycle_hooks_adds_handlers(
+    mock_server, mock_get_scene_details_json, mock_handle_save_state_response
+):
+    """Verify that lifecycle event handlers are registered."""
+    LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+    )
+    assert mock_server.controller.add_call_count >= 5
+
+
+def test_set_headers_sets_http_headers(
+    mock_server, mock_get_scene_details_json, mock_handle_save_state_response
+):
+    """Verify that the required HTTP headers are configured."""
+    LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+    )
+    mock_server.http_headers.set_header.assert_any_call(
+        "Access-Control-Allow-Origin", "*"
+    )
+    mock_server.http_headers.set_header.assert_any_call(
+        "Cache-Control", "max-age=0"
+    )
+
+
+@patch("ansys.visor.viewer.app.trame.local_app.settings")
+def test_set_www_path_sets_www(
+    mock_settings,
+    mock_server,
+    mock_get_scene_details_json,
+    mock_handle_save_state_response,
+):
+    """Verify that the client bundle path is assigned to the server."""
+    mock_settings.get_client_bundle.return_value = "/mock/path"
+    LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+    )
+    assert mock_server._www == "/mock/path"
+
+
+def test_get_visor_scene_details_json_returns_scene_details(
+    mock_server, mock_get_scene_details_json, mock_handle_save_state_response
+):
+    """Verify that scene details are returned from the callback."""
+    app = LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+    )
+    result = app.get_visor_scene_details_json()
+    assert result == {"scene": "details"}
+
+
+@patch("ansys.visor.viewer.app.trame.local_app.settings")
+def test_trame_logger_is_set_when_provided(
+    mock_settings,
+    mock_server,
+    mock_get_scene_details_json,
+    mock_handle_save_state_response,
+    mock_logger,
+):
+    """Verify that the provided logger is stored."""
+    mock_settings.get_client_bundle.return_value = "/mock/path"
+    app = LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+        trame_logger=mock_logger,
+    )
+    assert app._LocalApp__trame_logger == mock_logger
+
+
+@patch("ansys.visor.viewer.app.trame.local_app.settings")
+def test_trame_logger_is_none_when_not_provided(
+    mock_settings,
+    mock_server,
+    mock_get_scene_details_json,
+    mock_handle_save_state_response,
+):
+    """Verify that the logger is None when not provided."""
+    mock_settings.get_client_bundle.return_value = "/mock/path"
+    app = LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+    )
+    assert app._LocalApp__trame_logger is None
+
+
+@patch("ansys.visor.viewer.app.trame.local_app.settings")
+def test_lifecycle_logging_calls_debug(
+    mock_settings,
+    mock_server,
+    mock_get_scene_details_json,
+    mock_handle_save_state_response,
+    mock_logger,
+):
+    """Verify that lifecycle events generate debug log messages."""
+    mock_settings.get_client_bundle.return_value = "/mock/path"
+    LocalApp(
+        server=mock_server,
+        get_scene_details_json=mock_get_scene_details_json,
+        handle_save_state_response=mock_handle_save_state_response,
+        standalone=True,
+        trame_logger=mock_logger,
+    )
+
+    # Trigger each lifecycle event
+    mock_server.controller.handlers["on_server_start"](None)
+    mock_server.controller.handlers["on_server_ready"]()
+    mock_server.controller.handlers["on_client_connected"]()
+    mock_server.controller.handlers["on_client_exited"]()
+    mock_server.controller.handlers["on_server_exited"]()
+
+    # Check logger.debug was called with expected messages
+    mock_logger.debug.assert_any_call("Starting server")
+    mock_logger.debug.assert_any_call("Server is ready.")
+    mock_logger.debug.assert_any_call("Client connected.")
+    mock_logger.debug.assert_any_call("Client exited.")
+    mock_logger.debug.assert_any_call("Server is exiting.")
diff --git a/tests/unit/app/test_visor_vtk.py b/tests/unit/app/test_visor_vtk.py
new file mode 100644
index 00000000..8b4d74b7
--- /dev/null
+++ b/tests/unit/app/test_visor_vtk.py
@@ -0,0 +1,116 @@
+"""
+tests/unit/app/test_visor_vtk.py
+=================================
+Tests for VisorVTK.__new__ dispatch and abstract-method contract.
+
+Lifecycle behaviour is covered by test_visor_vtk_local.py; nothing here
+starts a trame server.  Mocking boundary is identical to that file:
+  - ansys.visor.viewer.app.visor_vtk.TrameServerManager
+  - ansys.visor.viewer.app.visor_vtk_local.LocalApp
+  - ansys.visor.viewer.app.visor_vtk_local.VisorLocalScene
+"""
+
+from contextlib import contextmanager
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer.app.visor_vtk import VisorVTK
+from ansys.visor.viewer.app.visor_vtk_local import VisorVTKLocal
+from ansys.visor.viewer.core.visor_enums import RenderingMode
+
+MODE_TO_CLASS = [
+    pytest.param(RenderingMode.LOCAL, VisorVTKLocal, id="LOCAL"),
+]
+
+
+@contextmanager
+def vtk_patches():
+    """Suppress all trame / VTK side-effects so no server is started."""
+    with patch("ansys.visor.viewer.app.visor_vtk.TrameServerManager") as mock_mgr, \
+         patch("ansys.visor.viewer.app.visor_vtk_local.LocalApp"), \
+         patch("ansys.visor.viewer.app.visor_vtk_local.VisorLocalScene") as mock_scene:
+        mock_mgr_inst = MagicMock()
+        mock_mgr_inst.running = False
+        mock_scene_inst = MagicMock()
+        mock_scene_inst.datasets = {}
+        mock_scene.return_value = mock_scene_inst
+        mock_mgr.return_value = mock_mgr_inst
+        yield mock_mgr, mock_scene
+
+
+def make_vtk(**kwargs) -> VisorVTK:
+    """Convenience wrapper: enter vtk_patches and return a VisorVTK instance."""
+    with vtk_patches():
+        return VisorVTK(**kwargs)
+
+
+@pytest.mark.parametrize("mode,expected_cls", MODE_TO_CLASS)
+def test_dispatch_returns_correct_subclass(mode, expected_cls):
+    """VisorVTK(rendering_mode=) returns an instance of expected_cls."""
+    with vtk_patches():
+        inst = VisorVTK(rendering_mode=mode)
+    assert isinstance(inst, expected_cls)
+
+
+def test_init_called_once_with_original_args():
+    """__init__ is invoked exactly once, forwarding the original call-site kwargs."""
+    with vtk_patches():
+        with patch.object(VisorVTK, "__init__", return_value=None) as mock_init:
+            VisorVTK(rendering_mode=RenderingMode.LOCAL, url="http://test", standalone=False)
+
+    mock_init.assert_called_once()
+    _, kwargs = mock_init.call_args
+    assert kwargs.get("rendering_mode") == RenderingMode.LOCAL
+    assert kwargs.get("url") == "http://test"
+    assert kwargs.get("standalone") is False
+
+
+def test_direct_construction_no_recursion():
+    """VisorVTKLocal() constructed directly never re-enters VisorVTK.__new__ dispatch."""
+    with vtk_patches():
+        with patch.object(VisorVTK, "__init__", return_value=None) as mock_init:
+            inst = VisorVTKLocal()
+
+    assert type(inst) is VisorVTKLocal
+    mock_init.assert_called_once()
+
+
+def test_unrecognized_mode_raises_value_error():
+    """A mode not present in the mode_map raises ValueError naming the bad mode."""
+    sentinel = MagicMock(name="FAKE_MODE")
+
+    with patch(
+        "ansys.visor.viewer.app.visor_vtk.RenderingMode.from_string",
+        return_value=sentinel,
+    ):
+        with pytest.raises(ValueError, match="Unsupported rendering mode for VTK"):
+            VisorVTK(rendering_mode=sentinel)
+
+
+def test_default_rendering_mode_dispatches_to_local():
+    """VisorVTK() with no rendering_mode argument falls back to LOCAL."""
+    with vtk_patches():
+        inst = VisorVTK()
+    assert isinstance(inst, VisorVTKLocal)
+
+
+def test_abstract_method_not_implemented_raises():
+    """A VisorVTK subclass that omits _initialize_rendering leaves the instance
+    without ``_scene``.  VisorVTK does not use ABCMeta (as proven by the
+    existing DummyIface fixture in test_visor_vtk_local.py), so Python does
+    not raise TypeError at instantiation time.  The *intended* failure instead
+    surfaces as AttributeError the moment any caller tries to use ``_scene``,
+    because ``_initialize_rendering`` never ran to create it.
+    """
+
+    class _Incomplete(VisorVTK):
+        pass  # deliberately does NOT implement _initialize_rendering
+
+    with vtk_patches():
+        inst = _Incomplete()
+
+    # _initialize_rendering was never called with a real body, so _scene is absent.
+    with pytest.raises(AttributeError):
+        _ = inst._scene  # noqa: F841
+
diff --git a/tests/unit/app/test_visor_vtk_local.py b/tests/unit/app/test_visor_vtk_local.py
new file mode 100644
index 00000000..a65221e1
--- /dev/null
+++ b/tests/unit/app/test_visor_vtk_local.py
@@ -0,0 +1,519 @@
+from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
+
+import pytest
+from vtkmodules.vtkCommonDataModel import vtkPolyData
+
+from ansys.visor.viewer.app.visor_vtk import (
+    VisorVTK,
+    require_input_is_not_none,
+    require_server_off,
+    require_server_on,
+    validate_input_metadata_types,
+)
+from ansys.visor.viewer.core.errors import ServerNotStartedError
+from ansys.visor.viewer.core.metadata import ExtendedMetadata
+from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
+from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState
+from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1
+
+
+@pytest.fixture
+def iface():
+    """Provide a VisorVTK instance with mocked dependencies."""
+    with patch("ansys.visor.viewer.app.visor_vtk.TrameServerManager") as mock_mgr, \
+         patch("ansys.visor.viewer.app.visor_vtk_local.LocalApp"), \
+         patch("ansys.visor.viewer.app.visor_vtk_local.VisorLocalScene") as mock_scene:
+        mock_mgr_inst = MagicMock()
+        mock_mgr_inst.running = True
+        mock_mgr_inst.state.model_dump.return_value = {"foo": "bar"}
+        mock_mgr_inst._server = MagicMock()
+        mock_mgr_inst.health = True
+        mock_scene_inst = MagicMock()
+        mock_scene_inst.datasets = {}
+        mock_scene.return_value = mock_scene_inst
+        mock_mgr.return_value = mock_mgr_inst
+        return VisorVTK()
+
+class DummyIface(VisorVTK):
+    @validate_input_metadata_types
+    def test_input_metadata_types(self, input=None, metadata=None):
+        """Verify that metadata validation succeeds for valid inputs."""
+        return "ok"
+
+    @require_input_is_not_none
+    def test_input_is_not_none(self, input=None, metadata=None):
+        """Verify that non-None inputs pass validation."""
+        return "ok"
+
+    @require_server_off
+    def test_require_server_off(self):
+        """Verify that execution succeeds when the server is not running."""
+        return "ok"
+
+    @require_server_on
+    def test_require_server_on(self):
+        """Verify that execution succeeds when the server is running."""
+        return "ok"
+
+@pytest.fixture
+def dummy_iface():
+    """Provide a DummyIface instance with mocked dependencies."""
+    with patch("ansys.visor.viewer.app.visor_vtk.TrameServerManager") as mock_mgr, \
+         patch("ansys.visor.viewer.app.visor_vtk_local.LocalApp"), \
+         patch("ansys.visor.viewer.app.visor_vtk_local.VisorLocalScene") as mock_scene:
+        mock_mgr_inst = MagicMock()
+        type(mock_mgr_inst).running = PropertyMock(return_value=True)
+        mock_mgr_inst.state.model_dump.return_value = {"foo": "bar"}
+        mock_mgr_inst._server = MagicMock()
+        mock_scene_inst = MagicMock()
+        mock_scene_inst.datasets = {}
+        mock_scene.return_value = mock_scene_inst
+        mock_mgr.return_value = mock_mgr_inst
+        return DummyIface()
+
+
+def test_running_property(iface):
+    """Verify that the running property reflects the server state."""
+    iface._server_manager.running = True
+    assert iface.running is True
+    iface._server_manager.running = False
+    assert iface.running is False
+
+def test_server_property(iface):
+    """Verify that the server property returns the managed server."""
+    assert iface.server is iface._server_manager.server
+
+def test_dataset_count_property(iface):
+    """Verify that the dataset count property delegates to the scene."""
+    type(iface._scene).dataset_count = PropertyMock(return_value=2)
+    assert iface.dataset_count == 2
+
+def test_start_calls_prepare_and_render_and_server_manager(iface):
+    """Verify that start prepares the scene and starts the server."""
+    iface._server_manager.running = False
+    iface._prepare_and_render_scene = MagicMock()
+    iface._server_manager.start = MagicMock()
+    iface.start("input", "metadata", timeout=5, blocking=True)
+    iface._prepare_and_render_scene.assert_called_once()
+    iface._server_manager.start.assert_called_once_with(5, True)
+
+def test_stop_calls_server_manager(iface):
+    """Verify that stop delegates to the server manager."""
+    iface._server_manager.running = True
+    iface._server_manager.stop = MagicMock()
+    iface.stop()
+    iface._server_manager.stop.assert_called_once()
+
+def test_update_clears_and_prepares_scene(iface):
+    """Verify that update clears the scene and reloads data."""
+    iface._server_manager.running = True
+    iface._scene.clear = MagicMock()
+    iface._prepare_and_render_scene = MagicMock()
+    iface.update("input", "metadata")
+    iface._scene.clear.assert_called_once()
+    iface._prepare_and_render_scene.assert_called_once()
+
+def test_add_dataset_calls_prepare_and_render(iface):
+    """Verify that adding a dataset prepares and renders the scene."""
+    iface._prepare_and_render_scene = MagicMock()
+    iface.add_dataset("input", "metadata")
+    iface._prepare_and_render_scene.assert_called_once()
+
+def test_remove_dataset_clears_if_no_datasets(iface):
+    """Verify that removing the last dataset clears the scene."""
+    # Mock that there's currently 1 dataset, which will become 0 after removal
+    type(iface._scene).dataset_count = PropertyMock(return_value=1)
+
+    iface._scene.remove_dataset = MagicMock()
+    iface.clear = MagicMock()
+    iface._scene.update_widgets = MagicMock()
+    iface._scene.render = MagicMock()
+
+    # Simulate that after removal, dataset_count becomes 0
+    def side_effect_remove(dataset_id):
+        type(iface._scene).dataset_count = PropertyMock(return_value=0)
+
+    iface._scene.remove_dataset.side_effect = side_effect_remove
+
+    # Remove the dataset - this should trigger clear since count will be 0 after removal
+    iface.remove_dataset(1)
+
+    # Assertions
+    iface._scene.remove_dataset.assert_called_once_with(1)
+    iface.clear.assert_called_once()
+
+def test_remove_dataset_updates_widgets_and_renders(iface):
+    """Verify that removing a dataset updates widgets and triggers a render."""
+    iface._scene.remove_dataset = MagicMock()
+    iface._scene.datasets = {1: MagicMock()}
+    iface.clear = MagicMock()
+    iface._scene.update_widgets = MagicMock()
+    iface._scene.render = MagicMock()
+    iface.remove_dataset(1)
+    iface._scene.update_widgets.assert_called_once()
+    iface._scene.render.assert_called_once()
+
+def test_list_datasets_delegates_to_scene(iface):
+    """Verify that dataset listing is delegated to the scene."""
+    # Arrange
+    expected_result = {1: {"name": "foo"}, 2: {"name": "bar"}}
+    iface._scene.list_all_dataset_info = MagicMock(return_value=expected_result)
+
+    # Act
+    result = iface.list_datasets()
+
+    # Assert
+    iface._scene.list_all_dataset_info.assert_called_once()
+    assert result == expected_result
+
+def test_clear_calls_scene_methods(iface):
+    """Verify that clear resets and finalizes the scene."""
+    iface._scene.clear = MagicMock()
+    iface._scene.finalize_scene = MagicMock()
+    iface.clear()
+    iface._scene.clear.assert_called_once()
+    iface._scene.finalize_scene.assert_called_once()
+
+def test_save_state_writes_json(tmp_path, iface):
+    """Verify that save_state writes viewer state to JSON."""
+    import json
+    from unittest.mock import MagicMock
+
+    # Arrange: fake state object returned by the (frontend-driven) get_state call
+    state = MagicMock()
+    state.model_dump_json.return_value = json.dumps({"foo": "bar"})
+
+    async def _get_state(*args, **kwargs):
+        return state
+
+    iface._scene.get_state = _get_state
+    iface._server_manager.running = True
+
+    # Act
+    import asyncio
+    asyncio.run(iface.save_state(str(tmp_path)))
+
+    # Assert
+    state_file = tmp_path / "visor.json"
+    assert state_file.exists()
+    with open(state_file) as f:
+        data = json.load(f)
+    assert data == {"foo": "bar"}
+
+def test_save_state_writes_dataset_snapshots(tmp_path, iface):
+    """Verify that dirty datasets are written as snapshots."""
+    import asyncio
+    from unittest.mock import MagicMock
+
+    # Arrange: two dirty mock datasets
+    ds1 = MagicMock()
+    ds1.name = "model_a"
+    ds1.data = MagicMock()
+    ds1.is_dirty = True
+    ds2 = MagicMock()
+    ds2.name = "model_b"
+    ds2.data = MagicMock()
+    ds2.is_dirty = True
+    iface._scene.datasets = {1: ds1, 2: ds2}
+
+    state = MagicMock()
+    state.scene.dataset_states = {}
+
+    async def _get_state(*args, **kwargs):
+        return state
+
+    iface._scene.get_state = _get_state
+    iface._server_manager.running = True
+    iface._file_io.write_dataset = MagicMock()
+    iface._file_io.write_state = MagicMock()
+
+    asyncio.run(iface.save_state(str(tmp_path)))
+
+    # One write_dataset call per dirty dataset
+    assert iface._file_io.write_dataset.call_count == 2
+    called_datasets = {call.args[1] for call in iface._file_io.write_dataset.call_args_list}
+    assert ds1.data in called_datasets
+    assert ds2.data in called_datasets
+    # Datasets should be marked clean after a successful write
+    ds1.mark_clean.assert_called_once()
+    ds2.mark_clean.assert_called_once()
+
+def test_save_state_skips_clean_datasets(tmp_path, iface):
+    """Verify that clean datasets are not written to disk."""
+    import asyncio
+    from unittest.mock import MagicMock
+
+    # Arrange: one dirty, one clean
+    ds_dirty = MagicMock()
+    ds_dirty.name = "model_a"
+    ds_dirty.data = MagicMock()
+    ds_dirty.is_dirty = True
+    ds_clean = MagicMock()
+    ds_clean.name = "model_b"
+    ds_clean.data = MagicMock()
+    ds_clean.is_dirty = False
+    iface._scene.datasets = {1: ds_dirty, 2: ds_clean}
+
+    state = MagicMock()
+    state.scene.dataset_states = {}
+
+    async def _get_state(*args, **kwargs):
+        return state
+
+    iface._scene.get_state = _get_state
+    iface._server_manager.running = True
+    iface._file_io.write_dataset = MagicMock()
+    iface._file_io.write_state = MagicMock()
+
+    asyncio.run(iface.save_state(str(tmp_path)))
+
+    # Only the dirty dataset should be written
+    assert iface._file_io.write_dataset.call_count == 1
+    assert iface._file_io.write_dataset.call_args.args[1] is ds_dirty.data
+    ds_dirty.mark_clean.assert_called_once()
+    ds_clean.mark_clean.assert_not_called()
+
+@pytest.mark.asyncio
+async def test__start_async_calls_prepare_and_server_manager(iface):
+    """Verify that asynchronous start prepares the scene and starts the server."""
+    iface._server_manager.running = False
+    iface._prepare_and_render_scene = MagicMock(return_value="dataset_id")
+    iface._server_manager.start_async = AsyncMock(return_value="task")
+    result = await iface._start_async("input", "metadata", timeout=1)
+    iface._prepare_and_render_scene.assert_called_once()
+    iface._server_manager.start_async.assert_awaited_once_with(1)
+    assert result == ("dataset_id", "task")
+
+@pytest.mark.asyncio
+async def test__stop_async_calls_server_manager(iface):
+    """Verify that asynchronous stop delegates to the server manager."""
+    iface._server_manager.running = True
+    iface._server_manager.stop_async = AsyncMock()
+    await iface._stop_async()
+    iface._server_manager.stop_async.assert_awaited_once()
+
+def test__health_returns_health(iface):
+    """Verify that health status is returned from the server manager."""
+    iface._server_manager.health = True
+    assert iface._health() is True
+
+def test__prepare_and_render_scene_calls_scene_methods(iface):
+    """Verify that scene preparation adds data and finalizes the scene."""
+    valid_metadata = ExtendedMetadata(name="test", unit="m", file_path="/path/to/file.vtp", metadata_path=None)
+    dummy_dataset = vtkPolyData()
+
+    # Mock dataset_count to return a valid integer
+    type(iface._scene).dataset_count = PropertyMock(return_value=1)
+
+    with patch.object(iface._file_io, "resolve", return_value=(dummy_dataset, valid_metadata)):
+        iface._scene.add_dataset = MagicMock()
+        iface._scene.finalize_scene = MagicMock()
+        iface._prepare_and_render_scene("input", "metadata")
+        iface._scene.add_dataset.assert_called_once()
+        iface._scene.finalize_scene.assert_called_once()
+
+
+def test__prepare_and_render_scene_with_none_input(iface):
+    """Verify that scene finalization occurs even when no input is provided."""
+    # Mock dataset_count to return 0 (no datasets)
+    type(iface._scene).dataset_count = PropertyMock(return_value=0)
+
+    iface._scene.add_dataset = MagicMock()
+    iface._scene.finalize_scene = MagicMock()
+    iface._prepare_and_render_scene(None, None)
+    iface._scene.add_dataset.assert_not_called()
+    iface._scene.finalize_scene.assert_called_once()
+
+def test_validate_input_metadata_types_raises_on_bad_input_type(dummy_iface):
+    """Verify that invalid input types raise a TypeError."""
+    with pytest.raises(TypeError, match="input must be a str or VisorDatasetType"):
+        dummy_iface.test_input_metadata_types(input=123)
+
+def test_validate_input_metadata_types_raises_on_bad_metadata_type(dummy_iface):
+    """Verify that invalid metadata types raise a TypeError."""
+    with pytest.raises(TypeError, match="metadata must be a string, an instance of Metadata or None"):
+        dummy_iface.test_input_metadata_types(input="valid", metadata=[])
+
+def test_require_input_is_not_none_raises_on_none_input(dummy_iface):
+    """Verify that a None input raises a ValueError."""
+    with pytest.raises(ValueError, match="Input cannot be None"):
+        dummy_iface.test_input_is_not_none(input=None)
+
+def test_require_server_off_raises_when_running(dummy_iface):
+    """Verify that execution is blocked when the server is already running."""
+    with pytest.raises(RuntimeError, match="Visor server already running"):
+        dummy_iface.test_require_server_off()
+
+def test_require_server_on_raises_when_not_running(dummy_iface):
+    """Verify that execution is blocked when the server is not running."""
+    type(dummy_iface._server_manager).running = PropertyMock(return_value=False)
+    print(f'dummy_iface._server_manager.running: {dummy_iface._server_manager.running}')
+    with pytest.raises(ServerNotStartedError, match="Failed to find running server"):
+        dummy_iface.test_require_server_on()
+
+def test_start_calls_scene_clear(iface):
+    """Verify that start clears the scene before loading new data."""
+    iface._server_manager.running = False
+    iface._prepare_and_render_scene = MagicMock()
+    iface._server_manager.start = MagicMock()
+    iface._scene.clear = MagicMock()
+    iface.start("input", "metadata", timeout=5, blocking=True)
+    iface._scene.clear.assert_called_once()
+
+@pytest.mark.asyncio
+async def test__start_async_calls_scene_clear(iface):
+    """Verify that asynchronous start clears the scene before loading new data."""
+    iface._server_manager.running = False
+    iface._prepare_and_render_scene = MagicMock()
+    iface._server_manager.start_async = AsyncMock(return_value="task")
+    iface._scene.clear = MagicMock()
+    await iface._start_async("input", "metadata", timeout=1)
+    iface._scene.clear.assert_called_once()
+
+def test_start_does_not_clear_scene_if_input_none(iface):
+    """Verify that start does not clear the scene when no input is provided."""
+    iface._server_manager.running = False  # Ensure server is not running
+    iface._scene.clear = MagicMock()
+    iface._prepare_and_render_scene = MagicMock()
+    iface._server_manager.start = MagicMock()
+    iface.start(None, None, timeout=5, blocking=True)
+    iface._scene.clear.assert_not_called()
+
+@pytest.mark.asyncio
+async def test__start_async_does_not_clear_scene_if_input_none(iface):
+    """Verify that asynchronous start does not clear the scene when no input is provided."""
+    iface._server_manager.running = False  # Ensure server is not running
+    iface._scene.clear = MagicMock()
+    iface._prepare_and_render_scene = MagicMock()
+    iface._server_manager.start_async = AsyncMock(return_value="task")
+    await iface._start_async(None, None, timeout=1)
+    iface._scene.clear.assert_not_called()
+
+def test_update_calls_scene_clear(iface):
+    """Verify that update clears the scene before reloading data."""
+    iface._server_manager.running = True
+    iface._scene.clear = MagicMock()
+    iface._prepare_and_render_scene = MagicMock()
+    iface.update("input", "metadata")
+
+
+# ================================================================== #
+# load_state
+# ================================================================== #
+
+def _make_state_with_datasets(snapshot_path: str | None, name="model", unit="m") -> PersistedViewerStateV1:
+    """Helper function to create a PersistedViewerStateV1 with a dataset state."""
+    ds_state = PersistedDatasetState(serialized_dataset_path=snapshot_path)
+    return PersistedViewerStateV1.from_components(
+        ui_state=VisorUIState(), unit=unit, orthographic_enabled=None,
+        cross_section_enabled=None, edges_enabled=None, bounding_box_enabled=None,
+        datasets={name: ds_state}
+    )
+
+
+def test_load_state_restores_datasets_when_registry_empty(tmp_path, iface):
+    """Verify that datasets are restored when no datasets are currently loaded."""
+    snapshot = tmp_path / "model_snapshot.vtkhdf"
+    snapshot.touch()
+    state = _make_state_with_datasets(str(snapshot), name="model")
+
+    mock_data = MagicMock()
+    iface._file_io.read_state = MagicMock(return_value=state)
+    iface._file_io.read_snapshot = MagicMock(return_value=mock_data)
+    iface._file_io.build_metadata_for_load_state = MagicMock(return_value=MagicMock(spec=ExtendedMetadata))
+    type(iface._scene).dataset_count = PropertyMock(side_effect=[0, 1])
+    loaded_ds = MagicMock()
+    iface._scene.add_dataset = MagicMock(return_value=42)
+    iface._scene.datasets = {42: loaded_ds}
+    iface._scene.apply_state = MagicMock()
+    iface._scene.finalize_scene = MagicMock()
+
+    iface.load_state(str(tmp_path))
+
+    iface._file_io.read_snapshot.assert_called_once_with(str(snapshot))
+    iface._scene.add_dataset.assert_called_once_with(mock_data, iface._file_io.build_metadata_for_load_state.return_value)
+    loaded_ds.mark_clean.assert_called_once()
+    iface._scene.finalize_scene.assert_called_once()
+    iface._scene.apply_state.assert_called_once_with(state)
+
+
+def test_load_state_skips_dataset_restore_when_registry_not_empty(tmp_path, iface):
+    """Verify that dataset restoration is skipped when datasets already exist."""
+    snapshot = tmp_path / "model_snapshot.vtkhdf"
+    snapshot.touch()
+    state = _make_state_with_datasets(str(snapshot), name="model")
+
+    iface._file_io.read_state = MagicMock(return_value=state)
+    iface._file_io.read_snapshot = MagicMock()
+    type(iface._scene).dataset_count = PropertyMock(return_value=1)
+    iface._scene.apply_state = MagicMock()
+    iface._scene.finalize_scene = MagicMock()
+
+    iface.load_state(str(tmp_path))
+
+    iface._file_io.read_snapshot.assert_not_called()
+    iface._scene.apply_state.assert_called_once_with(state)
+
+
+def test_load_state_skips_missing_snapshot(tmp_path, iface):
+    """Verify that missing dataset snapshots are ignored during state loading."""
+    state = _make_state_with_datasets("/nonexistent/snap.vtkhdf", name="model")
+
+    iface._file_io.read_state = MagicMock(return_value=state)
+    iface._file_io.read_snapshot = MagicMock()
+    type(iface._scene).dataset_count = PropertyMock(return_value=0)
+    iface._scene.apply_state = MagicMock()
+    iface._scene.finalize_scene = MagicMock()
+
+    iface.load_state(str(tmp_path))
+
+    iface._file_io.read_snapshot.assert_not_called()
+    iface._scene.finalize_scene.assert_not_called()
+    iface._scene.apply_state.assert_called_once_with(state)
+
+
+def test_load_state_skips_dataset_with_no_snapshot_path(tmp_path, iface):
+    """Verify that datasets without snapshot paths are ignored during state loading."""
+    state = _make_state_with_datasets(None, name="model")
+
+    iface._file_io.read_state = MagicMock(return_value=state)
+    iface._file_io.read_snapshot = MagicMock()
+    type(iface._scene).dataset_count = PropertyMock(return_value=0)
+    iface._scene.apply_state = MagicMock()
+    iface._scene.finalize_scene = MagicMock()
+
+    iface.load_state(str(tmp_path))
+
+    iface._file_io.read_snapshot.assert_not_called()
+    iface._scene.apply_state.assert_called_once_with(state)
+
+
+def test_load_state_passes_correct_metadata_to_add_dataset(tmp_path, iface):
+    """Verify that restored datasets receive the expected metadata."""
+    snapshot = tmp_path / "mesh_snapshot.vtkhdf"
+    snapshot.touch()
+    ds_state = PersistedDatasetState(
+        serialized_dataset_path=str(snapshot), source_file_path="/orig/mesh.vtu"
+    )
+    state = PersistedViewerStateV1.from_components(
+        ui_state=VisorUIState(), unit="mm", orthographic_enabled=None,
+        cross_section_enabled=None, edges_enabled=None, bounding_box_enabled=None,
+        datasets={"mesh": ds_state}
+    )
+
+    built_meta = MagicMock(spec=ExtendedMetadata)
+    mock_data = MagicMock()
+    iface._file_io.read_state = MagicMock(return_value=state)
+    iface._file_io.read_snapshot = MagicMock(return_value=mock_data)
+    iface._file_io.build_metadata_for_load_state = MagicMock(return_value=built_meta)
+    iface._scene.add_dataset = MagicMock(return_value=1)
+    type(iface._scene).dataset_count = PropertyMock(side_effect=[0, 1])
+    iface._scene.datasets = {1: MagicMock()}
+    iface._scene.apply_state = MagicMock()
+    iface._scene.finalize_scene = MagicMock()
+
+    iface.load_state(str(tmp_path))
+
+    iface._file_io.build_metadata_for_load_state.assert_called_once_with("mesh", "mm", ds_state)
+    iface._file_io.read_snapshot.assert_called_once_with(str(snapshot))
+    iface._scene.add_dataset.assert_called_once_with(mock_data, built_meta)
diff --git a/tests/unit/cli/test_apis.py b/tests/unit/cli/test_apis.py
new file mode 100644
index 00000000..47e756d6
--- /dev/null
+++ b/tests/unit/cli/test_apis.py
@@ -0,0 +1,203 @@
+import json
+from unittest.mock import mock_open, patch
+
+from ansys.visor.viewer.cli.apis import InstanceAPI, LogsAPI, ServerAPI
+from ansys.visor.viewer.core.visor_enums import RenderingMode
+
+## --- ServerAPI tests ---
+
+def test_serverapi_start_invokes_uvicorn_run():
+    """Verify that starting the server delegates to uvicorn.run."""
+    api = ServerAPI("host", 1234)
+    with patch("ansys.visor.viewer.cli.apis.uvicorn.run") as mock_run:
+        api.start()
+        mock_run.assert_called_once_with(
+            "ansys.visor.viewer.api.server:app",
+            host="host",
+            port=1234,
+            log_level="info",
+        )
+
+def test_serverapi_health_prints_response():
+    """Verify that the health endpoint response is printed."""
+    api = ServerAPI("host", 1234)
+    with patch("requests.get") as mock_get, patch("builtins.print") as mock_print:
+        mock_get.return_value.json.return_value = {"status": "ok"}
+        api.health()
+        mock_get.assert_called_once_with("http://host:1234/health")
+        mock_print.assert_called_with({"status": "ok"})
+
+def test_serverapi_info_prints_response():
+    """Verify that the info endpoint response is printed."""
+    api = ServerAPI("host", 1234)
+    with patch("requests.get") as mock_get, patch("builtins.print") as mock_print:
+        mock_get.return_value.json.return_value = {"info": "data"}
+        api.info()
+        mock_get.assert_called_once_with("http://host:1234/info")
+        mock_print.assert_called_with({"info": "data"})
+
+def test_serverapi_initialize_prints_response():
+    """Verify that the initialize response is printed."""
+    api = ServerAPI("host", 1234)
+    with patch("requests.post") as mock_post, patch("builtins.print") as mock_print:
+        mock_post.return_value.json.return_value = {"result": "ok"}
+        api.initialize("h", 1, RenderingMode.LOCAL, True, dark_mode=False)
+        mock_post.assert_called_once()
+        mock_print.assert_called_with({"result": "ok"})
+
+def test_serverapi_list_prints_urls():
+    """Verify that available instance URLs are printed."""
+    api = ServerAPI("host", 1234)
+    with patch("requests.get") as mock_get, patch("builtins.print") as mock_print:
+        mock_get.return_value.json.return_value = {"urls": ["url1", "url2"]}
+        api.list()
+        mock_print.assert_any_call("Available instances:")
+        mock_print.assert_any_call("  url1")
+        mock_print.assert_any_call("  url2")
+
+def test_serverapi_list_prints_nothing_if_no_urls():
+    """Verify that nothing is printed when no instances are available."""
+    api = ServerAPI("host", 1234)
+    with patch("requests.get") as mock_get, patch("builtins.print") as mock_print:
+        mock_get.return_value.json.return_value = {"urls": []}
+        api.list()
+        mock_print.assert_not_called()
+
+## --- InstanceAPI tests ---
+
+def test_instanceapi_start_prints_response():
+    """Verify that the start response is printed."""
+    api = InstanceAPI("host", 1234)
+    with patch("requests.post") as mock_post, patch("builtins.print") as mock_print:
+        mock_post.return_value.json.return_value = {"started": True}
+        api.start("file", "meta", 5)
+        mock_post.assert_called_once()
+        mock_print.assert_called_with({"started": True})
+
+def test_instanceapi_update_prints_response():
+    """Verify that the update response is printed."""
+    api = InstanceAPI("host", 1234)
+    with patch("requests.post") as mock_post, patch("builtins.print") as mock_print:
+        mock_post.return_value.json.return_value = {"updated": True}
+        api.update("file", "meta")
+        mock_post.assert_called_once()
+        mock_print.assert_called_with({"updated": True})
+
+def test_instanceapi_add_dataset_prints_response():
+    """Verify that the add_dataset response is printed."""
+    api = InstanceAPI("host", 1234)
+    with patch("requests.post") as mock_post, patch("builtins.print") as mock_print:
+        mock_post.return_value.json.return_value = {"added": True}
+        api.add_dataset("file", "meta")
+        mock_post.assert_called_once()
+        mock_print.assert_called_with({"added": True})
+
+def test_instanceapi_list_datasets_prints_json():
+    """Verify that datasets are printed as formatted JSON."""
+    api = InstanceAPI("host", 1234)
+    with patch("requests.get") as mock_get, patch("builtins.print") as mock_print:
+        mock_get.return_value.json.return_value = {"datasets": {"a": 1}}
+        api.list_datasets()
+        mock_print.assert_called_once_with(json.dumps({"a": 1}, indent=4))
+
+def test_instanceapi_remove_dataset_prints_response():
+    """Verify that the remove_dataset response is printed."""
+    api = InstanceAPI("host", 1234)
+    with patch("requests.post") as mock_post, patch("builtins.print") as mock_print:
+        mock_post.return_value.json.return_value = {"removed": True}
+        api.remove_dataset(42)
+        mock_post.assert_called_once()
+        mock_print.assert_called_with({"removed": True})
+
+def test_instanceapi_stop_visualization_prints_response():
+    """Verify that the stop_visualization response is printed."""
+    api = InstanceAPI("host", 1234)
+    with patch("requests.post") as mock_post, patch("builtins.print") as mock_print:
+        mock_post.return_value.json.return_value = {"stopped": True}
+        api.stop_visualization()
+        mock_post.assert_called_once()
+        mock_print.assert_called_with({"stopped": True})
+
+def test_instanceapi_stop_prints_response():
+    """Verify that the stop response is printed."""
+    api = InstanceAPI("host", 1234)
+    with patch("requests.post") as mock_post, patch("builtins.print") as mock_print:
+        mock_post.return_value.json.return_value = {"stopped": True}
+        api.stop()
+        mock_post.assert_called_once()
+        mock_print.assert_called_with({"stopped": True})
+
+## --- LogsAPI tests ---
+
+def test_logsapi_list_logs_prints_files(tmp_path):
+    """Verify that available log files are listed."""
+    log_dir = tmp_path
+    (log_dir / "foo.log").write_text("x")
+    (log_dir / "bar.log").write_text("y")
+    api = LogsAPI(str(log_dir))
+    with patch("builtins.print") as mock_print:
+        api.list_logs()
+        mock_print.assert_any_call("Available log files:")
+        mock_print.assert_any_call("  foo")
+        mock_print.assert_any_call("  bar")
+
+def test_logsapi_list_logs_prints_no_files(tmp_path):
+    """Verify that a message is printed when no log files exist."""
+    api = LogsAPI(str(tmp_path))
+    with patch("builtins.print") as mock_print:
+        api.list_logs()
+        mock_print.assert_any_call("No log files found.")
+
+def test_logsapi_list_logs_prints_dir_not_found():
+    """Verify that a missing log directory is reported."""
+    api = LogsAPI("not_a_dir")
+    with patch("builtins.print") as mock_print:
+        api.list_logs()
+        mock_print.assert_any_call("Log directory not found: not_a_dir")
+
+def test_logsapi_show_log_prints_last_lines():
+    """Verify that the requested tail of the log file is printed."""
+    api = LogsAPI()
+    log_content = "line1\nline2\nline3\n"
+    m = mock_open(read_data=log_content)
+    with patch("builtins.open", m), patch("builtins.print") as mock_print, patch("os.path.join", return_value="file.log"):
+        api.show_log("file", follow=False, lines=2)
+        # Should print last 2 lines
+        printed = "".join([call.args[0] for call in mock_print.call_args_list])
+        assert "line2" in printed and "line3" in printed
+
+def test_logsapi_show_log_file_not_found():
+    """Verify that a missing log file is reported."""
+    api = LogsAPI()
+    with patch("builtins.open", side_effect=FileNotFoundError), patch("builtins.print") as mock_print, patch("os.path.join", return_value="file.log"):
+        api.show_log("file")
+        mock_print.assert_any_call("Log file not found: file.log")
+
+def test_logsapi_show_log_follow_prints_and_waits(monkeypatch):
+    """Verify that follow mode continues monitoring the log file."""
+    api = LogsAPI()
+    log_content = "line1\nline2\nline3\n"
+    m = mock_open(read_data=log_content)
+    file_obj = m()
+    # Simulate file with 3 lines, then no new lines
+    lines = ["", "", ""]
+    def readline_side_effect():
+        return lines.pop(0) if lines else ""
+    file_obj.readline.side_effect = readline_side_effect
+    monkeypatch.setattr("builtins.open", lambda *a, **kw: file_obj)
+    monkeypatch.setattr("os.path.join", lambda *a, **kw: "file.log")
+
+    # Patch time.sleep and break after a few loops
+    call_count = {"count": 0}
+    def sleep_side_effect(_):
+        call_count["count"] += 1
+        if call_count["count"] > 3:
+            raise SystemExit()  # Stop the test
+
+    with patch("builtins.print") as mock_print, patch("time.sleep", side_effect=sleep_side_effect):
+        try:
+            api.show_log("file", follow=True, lines=2)
+        except SystemExit:
+            pass
+        printed = "".join([call.args[0] for call in mock_print.call_args_list])
+        assert "line2" in printed or "line3" in printed
diff --git a/tests/unit/cli/test_parse_args.py b/tests/unit/cli/test_parse_args.py
new file mode 100644
index 00000000..f4ba97ed
--- /dev/null
+++ b/tests/unit/cli/test_parse_args.py
@@ -0,0 +1,216 @@
+import sys
+from unittest.mock import patch
+
+import pytest
+
+from ansys.visor.viewer.cli.parse_args import parse_args
+from ansys.visor.viewer.cli.visor_cli import check_init_args
+from ansys.visor.viewer.core.visor_enums import RenderingMode
+
+
+def run_parse_args(argv):
+    with patch.object(sys, "argv", argv):
+        return parse_args()
+
+def test_server_start_args():
+    """Verify that server start arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "server", "start"])
+    assert args.group == "server"
+    assert args.action == "start"
+    assert args.api_host == "localhost"
+    assert args.api_port == 53211
+
+def test_server_health_args():
+    """Verify that server health arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "server", "health"])
+    assert args.group == "server"
+    assert args.action == "health"
+
+def test_server_info_args():
+    """Verify that server info arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "server", "info"])
+    assert args.group == "server"
+    assert args.action == "info"
+
+def test_server_init_args_defaults():
+    """Verify that server init uses the expected default argument values."""
+    args = run_parse_args(["visor-cli", "server", "init"])
+    assert args.group == "server"
+    assert args.action == "init"
+    assert args.host == "localhost"
+    assert args.standalone is None
+    assert args.port == 0
+    assert args.dark_mode is None
+
+def test_server_init_args_custom():
+    """Verify that custom server init arguments are parsed correctly."""
+    args = run_parse_args([
+        "visor-cli", "server", "init",
+        "--host", "myhost", "--port", "12345", "--standalone", "False"
+    ])
+    assert args.host == "myhost"
+    assert args.port == 12345
+    assert args.standalone is False
+
+def test_server_list_args():
+    """Verify that server list arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "server", "list"])
+    assert args.group == "server"
+    assert args.action == "list"
+
+def test_instance_start_args_defaults():
+    """Verify that instance start uses the expected default argument values."""
+    args = run_parse_args(["visor-cli", "instance", "start"])
+    assert args.group == "instance"
+    assert args.action == "start"
+    assert args.file_path is None
+    assert args.metadata_path is None
+    assert args.timeout == 0
+
+def test_instance_start_args_custom():
+    """Verify that custom instance start arguments are parsed correctly."""
+    args = run_parse_args([
+        "visor-cli", "instance", "start", "file.vtk",
+        "--metadata-path", "meta.json", "--timeout", "5"
+    ])
+    assert args.file_path == "file.vtk"
+    assert args.metadata_path == "meta.json"
+    assert args.timeout == 5
+
+def test_instance_update_args():
+    """Verify that instance update arguments are parsed correctly."""
+    args = run_parse_args([
+        "visor-cli", "instance", "update", "file2.vtk",
+        "--metadata-path", "meta2.json"
+    ])
+    assert args.action == "update"
+    assert args.file_path == "file2.vtk"
+    assert args.metadata_path == "meta2.json"
+
+def test_instance_add_dataset_args():
+    """Verify that add-dataset arguments are parsed correctly."""
+    args = run_parse_args([
+        "visor-cli", "instance", "add", "file3.vtk",
+        "--metadata-path", "meta3.json"
+    ])
+    assert args.action == "add"
+    assert args.file_path == "file3.vtk"
+    assert args.metadata_path == "meta3.json"
+
+def test_instance_list_datasets_args():
+    """Verify that list-datasets arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "instance", "list"])
+    assert args.action == "list"
+
+def test_instance_remove_dataset_args():
+    """Verify that remove-dataset arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "instance", "remove", "42"])
+    assert args.action == "remove"
+    assert args.dataset_id == 42
+
+def test_instance_stop_visualization_args():
+    """Verify that stop_visualization arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "instance", "stop_visualization"])
+    assert args.action == "stop_visualization"
+
+def test_instance_stop_args():
+    """Verify that instance stop arguments are parsed correctly."""
+    args = run_parse_args(["visor-cli", "instance", "stop"])
+    assert args.action == "stop"
+
+def test_logs_list_args_defaults():
+    """Verify that logs arguments use the expected default values."""
+    args = run_parse_args(["visor-cli", "logs"])
+    assert args.group == "logs"
+    assert args.log_name is None
+    assert args.follow is False
+    assert args.log_dir is None
+    assert args.lines == 10
+
+def test_logs_show_args_custom():
+    """Verify that custom log display arguments are parsed correctly."""
+    args = run_parse_args([
+        "visor-cli", "logs", "mylog", "-f", "--log-dir", "/tmp", "-n", "5"
+    ])
+    assert args.log_name == "mylog"
+    assert args.follow is True
+    assert args.log_dir == "/tmp"
+    assert args.lines == 5
+
+def test_missing_group_raises():
+    """Verify that omitting the command group raises SystemExit."""
+    with patch.object(sys, "argv", ["visor-cli"]):
+        with pytest.raises(SystemExit):
+            parse_args()
+
+def test_missing_action_raises():
+    """Verify that omitting the command action raises SystemExit."""
+    with patch.object(sys, "argv", ["visor-cli", "server"]):
+        with pytest.raises(SystemExit):
+            parse_args()
+
+def test_no_flags_does_not_call_requests(monkeypatch):
+    """Verify that no instance lookup occurs when no init flags are provided."""
+    def fail_if_called(*args, **kwargs):
+        raise AssertionError("requests.get should not be called when no flags are provided")
+
+    # Patch the requests.get used in the module to fail if invoked
+    monkeypatch.setattr(
+        "ansys.visor.viewer.cli.visor_cli.requests.get", fail_if_called
+    )
+
+    # Neither standalone nor dark_mode set -> should return cleanly and not call requests.get
+    check_init_args("api-host", 0, "host", 9000, RenderingMode.LOCAL, None, None)
+
+def test_flags_and_request_raises_warning(monkeypatch, capsys):
+    """Verify that request failures emit a warning without exiting."""
+    def raise_exc(*args, **kwargs):
+        raise Exception("boom")
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.cli.visor_cli.requests.get", raise_exc
+    )
+
+    # Should not raise SystemExit; should print a warning
+    check_init_args("api-host", 0, "host", 9000, RenderingMode.LOCAL, True, None)
+
+    captured = capsys.readouterr()
+    assert "Warning: could not check existing instances: boom" in captured.out
+
+def test_existing_instance_triggers_exit(monkeypatch, capsys):
+    """Verify that initialization exits when the target instance already exists."""
+    class FakeResp:
+        def json(self):
+            return {"urls": ["http://target-host:1234"]}
+
+    def fake_get(url, timeout=5):
+        return FakeResp()
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.cli.visor_cli.requests.get", fake_get
+    )
+
+    with pytest.raises(SystemExit) as excinfo:
+        check_init_args("api-host", 0, "target-host", 1234, RenderingMode.LOCAL, True, None)
+
+    # sys.exit(1) should produce SystemExit with code 1
+    assert excinfo.value.code == 1
+
+    captured = capsys.readouterr()
+    assert "Error: Instance already initialized on http://target-host:1234." in captured.out
+
+def test_flags_but_no_existing_instance(monkeypatch):
+    """Verify that initialization proceeds when no matching instance exists."""
+    class FakeResp:
+        def json(self):
+            return {"urls": ["http://other:1111"]}
+
+    def fake_get(url, timeout=5):
+        return FakeResp()
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.cli.visor_cli.requests.get", fake_get
+    )
+
+    # Should not raise
+    check_init_args("api-host", 0, "target-host", 1234, RenderingMode.LOCAL, True, False)
diff --git a/tests/unit/cli/test_visor_cli.py b/tests/unit/cli/test_visor_cli.py
new file mode 100644
index 00000000..a304efb0
--- /dev/null
+++ b/tests/unit/cli/test_visor_cli.py
@@ -0,0 +1,156 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+import ansys.visor.viewer.cli.visor_cli as visor_cli
+from ansys.visor.viewer.core.visor_enums import RenderingMode
+
+
+@pytest.fixture
+def mock_parse_args():
+    """Provide a mocked parse_args function."""
+    with patch("ansys.visor.viewer.cli.visor_cli.parse_args") as mock:
+        yield mock
+
+@pytest.fixture
+def mock_server_api():
+    """Provide a mocked ServerAPI class."""
+    with patch("ansys.visor.viewer.cli.visor_cli.ServerAPI") as mock:
+        yield mock
+
+@pytest.fixture
+def mock_instance_api():
+    """Provide a mocked InstanceAPI class."""
+    with patch("ansys.visor.viewer.cli.visor_cli.InstanceAPI") as mock:
+        yield mock
+
+@pytest.fixture
+def mock_logs_api():
+    """Provide a mocked LogsAPI class."""
+    with patch("ansys.visor.viewer.cli.visor_cli.LogsAPI") as mock:
+        yield mock
+
+@pytest.fixture
+def mock_requests_get():
+    """Provide a mocked requests.get function."""
+    with patch("ansys.visor.viewer.cli.visor_cli.requests.get") as mock:
+        yield mock
+
+def test_check_server_running_success(mock_requests_get):
+    """Verify that a healthy server returns True."""
+    mock_resp = MagicMock()
+    mock_resp.status_code = 200
+    mock_requests_get.return_value = mock_resp
+    assert visor_cli.check_server_running("host", 1234) is True
+
+def test_check_server_running_failure_status(mock_requests_get, capsys):
+    """Verify that a failed health check returns False and reports an error."""
+    mock_resp = MagicMock()
+    mock_resp.status_code = 500
+    mock_resp.text = "fail"
+    mock_requests_get.return_value = mock_resp
+    assert visor_cli.check_server_running("host", 1234) is False
+    out = capsys.readouterr().out
+    assert "Server health check failed" in out
+
+def test_check_server_running_exception(mock_requests_get, capsys):
+    """Verify that connection exceptions return False and print a warning."""
+    mock_requests_get.side_effect = Exception("boom")
+    assert visor_cli.check_server_running("host", 1234) is False
+    out = capsys.readouterr().out
+    assert "Could not connect to server" in out
+
+def make_args(group, action, **kwargs):
+    args = MagicMock()
+    args.group = group
+    args.action = action
+    args.api_host = "host"
+    args.api_port = 1234
+    for k, v in kwargs.items():
+        setattr(args, k, v)
+    return args
+
+def test_main_server_start_health(mock_parse_args, mock_server_api):
+    """Verify that server start and health actions invoke the correct API methods."""
+    for action in ["start", "health"]:
+        args = make_args("server", action)
+        mock_parse_args.return_value = args
+        api = MagicMock()
+        mock_server_api.return_value = api
+        with patch("ansys.visor.viewer.cli.visor_cli.check_server_running"):
+            visor_cli.main()
+            getattr(api, action).assert_called_once()
+
+def test_main_server_info_list(mock_parse_args, mock_server_api):
+    """Verify that server info and list actions invoke the correct API methods."""
+    for action in ["info", "list"]:
+        args = make_args("server", action)
+        mock_parse_args.return_value = args
+        api = MagicMock()
+        mock_server_api.return_value = api
+        with patch("ansys.visor.viewer.cli.visor_cli.check_server_running", return_value=True):
+            visor_cli.main()
+            getattr(api, action).assert_called_once()
+
+def test_main_server_init(mock_parse_args, mock_server_api):
+    """Verify that server initialization forwards the expected arguments."""
+    args = make_args("server", "init", host="h", port=42, rendering_mode=RenderingMode.LOCAL, standalone=True, dark_mode=False)
+    mock_parse_args.return_value = args
+    api = MagicMock()
+    mock_server_api.return_value = api
+    with patch("ansys.visor.viewer.cli.visor_cli.check_server_running", return_value=True):
+        visor_cli.main()
+        api.initialize.assert_called_once_with("h", 42, RenderingMode.LOCAL, True, False)
+
+def test_main_instance_actions(mock_parse_args, mock_instance_api):
+    """Verify that instance actions invoke the corresponding API methods."""
+    actions = [
+        ("start", "start", {"file_path": "f", "metadata_path": "m", "timeout": 1}),
+        ("update", "update", {"file_path": "f", "metadata_path": "m"}),
+        ("add", "add_dataset", {"file_path": "f", "metadata_path": "m"}),
+        ("remove", "remove_dataset", {"dataset_id": 99}),
+        ("list", "list_datasets", {}),
+        ("stop_visualization", "stop_visualization", {}),
+        ("stop", "stop", {}),
+    ]
+
+    for action, method_name, extra in actions:
+        args = make_args("instance", action, **extra)
+        mock_parse_args.return_value = args
+        api = MagicMock()
+        mock_instance_api.return_value = api
+        with patch("ansys.visor.viewer.cli.visor_cli.check_server_running", return_value=True):
+            visor_cli.main()
+            method = getattr(api, method_name)
+            method.assert_called_once()
+
+def test_main_logs_list(mock_parse_args, mock_logs_api):
+    """Verify that log listing invokes the list_logs method."""
+    args = make_args("logs", None, log_name=None, follow=False, log_dir=None, lines=10)
+    mock_parse_args.return_value = args
+    api = MagicMock()
+    mock_logs_api.return_value = api
+    with patch("ansys.visor.viewer.cli.visor_cli.check_server_running", return_value=True):
+        visor_cli.main()
+        api.list_logs.assert_called_once()
+
+def test_main_logs_show(mock_parse_args, mock_logs_api):
+    """Verify that log display invokes show_log with the requested options."""
+    args = make_args("logs", None, log_name="mylog", follow=True, log_dir="dir", lines=5)
+    mock_parse_args.return_value = args
+    api = MagicMock()
+    mock_logs_api.return_value = api
+    with patch("ansys.visor.viewer.cli.visor_cli.check_server_running", return_value=True):
+        visor_cli.main()
+        api.show_log.assert_called_once_with("mylog", True, 5)
+
+def test_main_server_not_running_exits(mock_parse_args, capsys):
+    """Verify that instance commands exit when the server is not running."""
+    args = make_args("instance", "start", file_path="f", metadata_path="m", timeout=1)
+    mock_parse_args.return_value = args
+    with patch("ansys.visor.viewer.cli.visor_cli.check_server_running", return_value=False):
+        with pytest.raises(SystemExit) as e:
+            visor_cli.main()
+        assert e.value.code == 1
+    out = capsys.readouterr().out
+    assert "Server is not running" in out
diff --git a/tests/unit/core/test_errors.py b/tests/unit/core/test_errors.py
new file mode 100644
index 00000000..a1e75347
--- /dev/null
+++ b/tests/unit/core/test_errors.py
@@ -0,0 +1,70 @@
+import ansys.visor.viewer.core.errors as errors
+
+
+def test_invalid_file_error_str():
+    """Verify that InvalidFileError formats its message and stores the value."""
+    e = errors.InvalidFileError("foo.txt")
+    assert str(e) == repr("Invalid file: foo.txt")
+    assert e.value == "foo.txt"
+
+def test_invalid_server_timeout_error_str():
+    """Verify that InvalidServerTimeoutError formats its message and stores the value."""
+    e = errors.InvalidServerTimeoutError(42)
+    assert str(e) == repr("Invalid timeout value: 42")
+    assert e.value == 42
+
+def test_invalid_url_error_str():
+    """Verify that InvalidUrlError formats its message and stores the value."""
+    e = errors.InvalidUrlError("bad_url")
+    assert str(e) == repr("Invalid url: bad_url")
+    assert e.value == "bad_url"
+
+def test_invalid_path_error_str():
+    """Verify that InvalidPathError formats its message and stores the value."""
+    e = errors.InvalidPathError("/bad/path")
+    assert str(e) == repr("Invalid path: /bad/path")
+    assert e.value == "/bad/path"
+
+def test_invalid_file_format_error_str():
+    """Verify that InvalidFileFormatError formats its message and stores the value."""
+    e = errors.InvalidFileFormatError(".bad")
+    assert str(e) == repr("Invalid file format: .bad")
+    assert e.value == ".bad"
+
+def test_server_not_started_error_str():
+    """Verify that ServerNotStartedError formats its message and stores the value."""
+    e = errors.ServerNotStartedError("not running")
+    assert str(e) == repr("Server not started: not running")
+    assert e.value == "not running"
+
+def test_visor_add_dataset_error_str_no_paths():
+    """Verify that VisorAddDatasetError returns the base message when no paths are provided."""
+    e = errors.VisorAddDatasetError("failed")
+    assert str(e) == "failed"
+    assert e.input_path is None
+    assert e.metadata_path is None
+
+def test_visor_add_dataset_error_str_with_input_path():
+    """Verify that VisorAddDatasetError includes the input path in the message."""
+    e = errors.VisorAddDatasetError("failed", input_path="input.vtk")
+    assert str(e) == "failed (input_path=input.vtk)"
+    assert e.input_path == "input.vtk"
+    assert e.metadata_path is None
+
+def test_visor_add_dataset_error_str_with_metadata_path():
+    """Verify that VisorAddDatasetError includes the metadata path in the message."""
+    e = errors.VisorAddDatasetError("failed", metadata_path="meta.json")
+    assert str(e) == "failed (metadata_path=meta.json)"
+    assert e.input_path is None
+    assert e.metadata_path == "meta.json"
+
+def test_visor_add_dataset_error_str_with_both_paths():
+    """Verify that VisorAddDatasetError includes both paths in the message."""
+    e = errors.VisorAddDatasetError(
+        "failed",
+        input_path="input.vtk",
+        metadata_path="meta.json",
+    )
+    assert str(e) == "failed (input_path=input.vtk, metadata_path=meta.json)"
+    assert e.input_path == "input.vtk"
+    assert e.metadata_path == "meta.json"
diff --git a/tests/unit/core/test_metadata.py b/tests/unit/core/test_metadata.py
new file mode 100644
index 00000000..999a7b5a
--- /dev/null
+++ b/tests/unit/core/test_metadata.py
@@ -0,0 +1,84 @@
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.core.metadata import ExtendedMetadata, Metadata
+from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState
+
+
+class DummyVisorState(PersistedDatasetState):
+    pass
+
+def test_metadata_valid_construction():
+    """Verify that Metadata stores the provided values."""
+    state = DummyVisorState(parts={})
+    m = Metadata(name="foo", unit="m", state=state)
+    assert m.name == "foo"
+    assert m.unit == "m"
+    assert m.state == state
+
+def test_metadata_state_dict_conversion():
+    """Verify that a state dictionary is converted to PersistedDatasetState."""
+    state_dict = {"parts": {}}
+    m = Metadata(name="foo", unit="m", state=state_dict)
+    assert isinstance(m.state, PersistedDatasetState)
+
+def test_metadata_state_invalid_raises():
+    """Verify that invalid state data raises a ValidationError."""
+    # 'parts' is required and should be a dict, so passing a wrong type should fail
+    with pytest.raises(ValidationError):
+        Metadata(name="foo", unit="m", state={"parts": 123})
+
+def test_metadata_defaults():
+    """Verify that Metadata creates a default state when one is not provided."""
+    m = Metadata(name="bar", unit="cm")
+    assert isinstance(m.state, PersistedDatasetState)
+
+def test_metadata_repr_and_eq():
+    """Verify that Metadata supports repr generation and equality comparison."""
+    state = DummyVisorState(parts={})
+    m1 = Metadata(name="foo", unit="m", state=state)
+    m2 = Metadata(name="foo", unit="m", state=state)
+    assert repr(m1)
+    assert m1 == m2
+
+def test_extended_metadata_valid_construction():
+    """Verify that ExtendedMetadata stores all provided values."""
+    state = DummyVisorState(parts={})
+    ext = ExtendedMetadata(
+        name="foo",
+        unit="m",
+        state=state,
+        file_path="file.txt",
+        metadata_path="meta.json"
+    )
+    assert ext.name == "foo"
+    assert ext.unit == "m"
+    assert ext.state == state
+    assert ext.file_path == "file.txt"
+    assert ext.metadata_path == "meta.json"
+
+def test_extended_metadata_defaults():
+    """Verify that ExtendedMetadata applies the expected default values."""
+    ext = ExtendedMetadata(name="bar", unit="cm")
+    assert ext.file_path is None
+    assert ext.metadata_path is None
+    assert isinstance(ext.state, PersistedDatasetState)
+
+def test_extended_metadata_state_dict_conversion():
+    """Verify that a state dictionary is converted to PersistedDatasetState."""
+    state_dict = {"parts": {}}
+    ext = ExtendedMetadata(name="foo", unit="m", state=state_dict)
+    assert isinstance(ext.state, PersistedDatasetState)
+
+def test_extended_metadata_state_invalid_raises():
+    """Verify that invalid state data raises a ValidationError."""
+    with pytest.raises(ValidationError):
+        ExtendedMetadata(name="foo", unit="m", state={"parts": 123})
+
+def test_extended_metadata_repr_and_eq():
+    """Verify that ExtendedMetadata supports repr generation and equality comparison."""
+    state = DummyVisorState(parts={})
+    ext1 = ExtendedMetadata(name="foo", unit="m", state=state, file_path="a", metadata_path="b")
+    ext2 = ExtendedMetadata(name="foo", unit="m", state=state, file_path="a", metadata_path="b")
+    assert repr(ext1)
+    assert ext1 == ext2
diff --git a/tests/unit/core/test_net.py b/tests/unit/core/test_net.py
new file mode 100644
index 00000000..65f03e33
--- /dev/null
+++ b/tests/unit/core/test_net.py
@@ -0,0 +1,122 @@
+"""Tests for ansys.visor.viewer.core.net."""
+
+import socket
+from unittest.mock import patch
+
+from ansys.visor.viewer.core.net import find_unused_port, is_user_admin
+
+
+def test_find_unused_port_returns_int_in_valid_range():
+    """A successful probe returns a bindable, non-privileged port when the caller is non-admin."""
+    with patch("ansys.visor.viewer.core.net.is_user_admin", return_value=False):
+        port = find_unused_port()
+    assert isinstance(port, int)
+    assert 1024 <= port <= 65535
+
+
+def test_find_unused_port_returns_bindable_port():
+    """The returned port must actually be bindable immediately after selection."""
+    port = find_unused_port()
+    assert port is not None
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    try:
+        sock.bind(("", port))
+    finally:
+        sock.close()
+
+
+def test_find_unused_port_avoids_listed_ports():
+    """A port present in ``avoid`` must not be returned."""
+    first = find_unused_port()
+    assert first is not None
+    # ask again while avoiding the first one; must get something different
+    second = find_unused_port(avoid=[first])
+    assert second is not None
+    assert second != first
+
+
+def test_find_unused_port_probes_on_specified_host():
+    """The probe must bind on the host supplied by the caller."""
+    captured_binds = []
+
+    real_socket = socket.socket
+
+    class _FakeSocket:
+        def __init__(self, *_a, **_kw):
+            self._inner = real_socket(socket.AF_INET, socket.SOCK_STREAM)
+
+        def bind(self, addr):
+            captured_binds.append(addr)
+            self._inner.bind(addr)
+
+        def getsockname(self):
+            return self._inner.getsockname()
+
+        def close(self):
+            self._inner.close()
+
+    with patch("ansys.visor.viewer.core.net.socket.socket", _FakeSocket):
+        find_unused_port(host="127.0.0.1")
+
+    assert captured_binds
+    assert all(addr[0] == "127.0.0.1" for addr in captured_binds)
+
+
+def test_find_unused_port_returns_none_when_all_candidates_avoided():
+    """If every OS-picked candidate is excluded, the function returns None."""
+    # Force the loop by making every port fall into ``avoid``. We can't
+    # enumerate every ephemeral port up front, so instead patch the socket
+    # so that ``getsockname`` always reports the same port, and put that
+    # port in ``avoid``.
+    fixed_port = 40000
+
+    class _FakeSocket:
+        def __init__(self, *_a, **_kw):
+            pass
+
+        def bind(self, _addr):
+            pass
+
+        def getsockname(self):
+            return ("", fixed_port)
+
+        def close(self):
+            pass
+
+    with patch("ansys.visor.viewer.core.net.socket.socket", _FakeSocket):
+        result = find_unused_port(avoid=[fixed_port])
+
+    assert result is None
+
+
+def test_find_unused_port_skips_privileged_for_non_admin():
+    """A non-admin caller must never receive a privileged port (< 1024)."""
+    ports_iter = iter([80, 443, 55555])  # first two privileged, third valid
+
+    class _FakeSocket:
+        def __init__(self, *_a, **_kw):
+            pass
+
+        def bind(self, _addr):
+            self._port = next(ports_iter)
+
+        def getsockname(self):
+            return ("", self._port)
+
+        def close(self):
+            pass
+
+    with patch("ansys.visor.viewer.core.net.socket.socket", _FakeSocket), \
+         patch("ansys.visor.viewer.core.net.is_user_admin", return_value=False):
+        result = find_unused_port()
+
+    assert result == 55555
+
+
+def test_is_user_admin_returns_bool():
+    """``is_user_admin`` must always return a boolean-compatible value without raising."""
+    result = is_user_admin()
+    # On Windows non-admin it's False; on POSIX it's the effective UID (int, 0 for root).
+    # In all cases we should get something truth-testable without an exception.
+    assert result is not None or result is None  # smoke: just ensure it ran
+    bool(result)  # must not raise
diff --git a/tests/unit/core/test_perf_timer.py b/tests/unit/core/test_perf_timer.py
new file mode 100644
index 00000000..0f27d779
--- /dev/null
+++ b/tests/unit/core/test_perf_timer.py
@@ -0,0 +1,237 @@
+
+from ansys.visor.viewer.core.perf_timer import PerfTimer
+
+# ------------------------------------------------------------------
+# Helpers
+# ------------------------------------------------------------------
+
+class DummyLogger:
+    """ A simple logger that records messages for testing purposes."""
+    def __init__(self):
+        self.messages = []
+
+    def info(self, msg):
+        self.messages.append(msg)
+
+
+# ------------------------------------------------------------------
+# disabled()
+# ------------------------------------------------------------------
+
+def test_disabled_timer_is_noop():
+    """disabled() should create a timer that does nothing."""
+
+    timer = PerfTimer.disabled()
+
+    # phase should not record anything
+    with timer.phase("x"):
+        pass
+
+    # log should not fail or emit anything
+    timer.log()
+
+    assert True  # no-op behavior (no exceptions)
+
+
+# ------------------------------------------------------------------
+# phase recording
+# ------------------------------------------------------------------
+
+def test_phase_records_when_enabled(monkeypatch):
+    """phase should record elapsed time when enabled."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        True,
+    )
+
+    # control perf_counter
+    times = iter([1.0, 2.0])
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.time.perf_counter",
+        lambda: next(times),
+    )
+
+    timer = PerfTimer("label", logger=DummyLogger())
+
+    with timer.phase("test"):
+        pass
+
+    # internal behavior exercised via log output
+    timer.log()
+
+    assert len(timer._phases) == 1
+    assert timer._phases[0][0] == "test"
+    assert abs(timer._phases[0][1] - 1.0) < 1e-6
+
+
+def test_phase_is_noop_when_disabled(monkeypatch):
+    """phase should not record when disabled."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        False,
+    )
+
+    timer = PerfTimer("label", logger=DummyLogger())
+
+    with timer.phase("test"):
+        pass
+
+    assert timer._phases == []
+
+
+# ------------------------------------------------------------------
+# logging behavior
+# ------------------------------------------------------------------
+
+def test_log_emits_expected_format(monkeypatch):
+    """log should emit correctly formatted message."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        True,
+    )
+
+    times = iter([1.0, 2.0])
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.time.perf_counter",
+        lambda: next(times),
+    )
+
+    logger = DummyLogger()
+    timer = PerfTimer("my.label", logger, foo=1)
+
+    with timer.phase("p"):
+        pass
+
+    timer.log()
+
+    assert len(logger.messages) == 1
+    msg = logger.messages[0]
+
+    assert "[PERF] my.label foo=1" in msg
+    assert "p=1.0000s" in msg
+
+
+def test_log_includes_total(monkeypatch):
+    """log should include total when requested."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        True,
+    )
+
+    times = iter([1.0, 2.0, 3.0, 5.0])  # two phases: 1s + 2s
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.time.perf_counter",
+        lambda: next(times),
+    )
+
+    logger = DummyLogger()
+    timer = PerfTimer("label", logger)
+
+    with timer.phase("a"):
+        pass
+    with timer.phase("b"):
+        pass
+
+    timer.log(include_total=True)
+
+    msg = logger.messages[0]
+
+    assert "a=1.0000s" in msg
+    assert "b=2.0000s" in msg
+    assert "total=3.0000s" in msg
+
+
+def test_log_skips_when_disabled(monkeypatch):
+    """log should not emit when logging disabled."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        False,
+    )
+
+    logger = DummyLogger()
+    timer = PerfTimer("label", logger)
+
+    with timer.phase("x"):
+        pass
+
+    timer.log()
+
+    assert logger.messages == []
+
+
+def test_log_skips_without_logger(monkeypatch):
+    """log should not emit when logger is None."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        True,
+    )
+
+    timer = PerfTimer("label", logger=None)
+
+    with timer.phase("x"):
+        pass
+
+    # should not error
+    timer.log()
+
+    assert True
+
+
+# ------------------------------------------------------------------
+# extra fields
+# ------------------------------------------------------------------
+
+def test_extra_fields_in_label(monkeypatch):
+    """Extra fields should be appended to label."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        True,
+    )
+
+    times = iter([1.0, 2.0])
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.time.perf_counter",
+        lambda: next(times),
+    )
+
+    logger = DummyLogger()
+    timer = PerfTimer("label", logger, a=1, b=2)
+
+    with timer.phase("p"):
+        pass
+
+    timer.log()
+
+    msg = logger.messages[0]
+
+    assert "a=1" in msg
+    assert "b=2" in msg
+
+
+# ------------------------------------------------------------------
+# enabled flag interaction
+# ------------------------------------------------------------------
+
+def test_enabled_flag_overrides_global(monkeypatch):
+    """Per-instance enabled=False should disable recording even if global is True."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.core.perf_timer.settings.perf_logging",
+        True,
+    )
+
+    timer = PerfTimer("label", logger=DummyLogger(), enabled=False)
+
+    with timer.phase("x"):
+        pass
+
+    timer.log()
+
+    assert timer._phases == []
diff --git a/tests/unit/core/test_visor_helpers.py b/tests/unit/core/test_visor_helpers.py
new file mode 100644
index 00000000..8f2044e6
--- /dev/null
+++ b/tests/unit/core/test_visor_helpers.py
@@ -0,0 +1,59 @@
+import pytest
+
+from ansys.visor.viewer.core import visor_helpers
+
+
+def test_get_random_javascript_safe_id_range_and_length():
+    """Verify that generated JavaScript-safe IDs have the expected range and length."""
+    for _ in range(10):
+        val = visor_helpers.get_random_javascript_safe_id()
+        assert 1000000000000000 <= val <= 9007199254740991
+        assert len(str(val)) == 16
+
+@pytest.mark.parametrize("url", [
+    "http://localhost:8080",
+    "http://127.0.0.1:5000",
+    "http://example.com",
+    "http://my-server.local",
+])
+def test_validate_url_valid(url):
+    """Verify that valid URLs are accepted."""
+    assert visor_helpers.validate_url(url) is not None
+
+@pytest.mark.parametrize("url", [
+    "ftp://example.com",
+    "localhost:8080",
+    "127.0.0.1",
+    "example.com",
+    "http:/bad.com",
+    "",
+    None,
+])
+def test_validate_url_invalid(url):
+    """Verify that invalid URLs are rejected."""
+    assert visor_helpers.validate_url(url) is None
+
+@pytest.mark.parametrize("host", [
+    "localhost",
+    "127.0.0.1",
+    "192.168.1.1",
+    "example.com",
+    "sub.domain.co.uk",
+    "my-server.local",
+])
+def test_validate_host_valid(host):
+    """Verify that valid host names are accepted."""
+    assert visor_helpers.validate_host(host) is True
+
+@pytest.mark.parametrize("host", [
+    "http://localhost",
+    "http://example.com",
+    "localhost:8080",
+    "127.0.0.1:5000",
+    "bad host",
+    "",
+    None,
+])
+def test_validate_host_invalid(host):
+    """Verify that invalid host names are rejected."""
+    assert visor_helpers.validate_host(host) is False
diff --git a/tests/unit/core/test_visor_logging.py b/tests/unit/core/test_visor_logging.py
new file mode 100644
index 00000000..78a2adf7
--- /dev/null
+++ b/tests/unit/core/test_visor_logging.py
@@ -0,0 +1,110 @@
+import logging
+from pathlib import Path
+
+from ansys.visor.viewer.core.visor_logging import (
+    VisorDefaultLogger,
+    VisorLogger,
+    configure_trame_logging,
+    set_up_named_logger,
+)
+
+
+def test_get_file_path_returns_correct_path(tmp_path):
+    """Verify that get_file_path returns the expected log file path."""
+    filename = "test.log"
+    logger = VisorLogger("test_logger", filename, log_dir=tmp_path)
+    expected_path = tmp_path / filename
+    assert logger.get_file_path() == expected_path
+
+def test_create_dir_creates_directory(tmp_path):
+    """Verify that the log directory is created when it does not exist."""
+    log_dir = tmp_path / "logs"
+    VisorLogger("test_logger", "file.log", log_dir=log_dir)
+    assert log_dir.exists() and log_dir.is_dir()
+
+def test_logger_adds_file_handler(tmp_path):
+    """Verify that a file handler is added for the configured log file."""
+    logger = VisorLogger("test_logger", "log.txt", log_dir=tmp_path)
+    file_paths = [h.baseFilename for h in logger.handlers if isinstance(h, logging.FileHandler)]
+    assert str(tmp_path / "log.txt") in file_paths
+
+def test_default_logger_uses_expected_filename(tmp_path):
+    """Verify that VisorDefaultLogger uses the default log filename."""
+    logger = VisorDefaultLogger("my_logger")
+    assert logger.filename == "visor.log"
+
+def test_formatter_applied(tmp_path):
+    """Verify that the expected formatter is applied to the file handler."""
+    logger = VisorLogger("formatter_logger", "log.txt", log_dir=tmp_path)
+    file_handler = next(h for h in logger.handlers if isinstance(h, logging.FileHandler))
+    assert isinstance(file_handler.formatter, logging.Formatter)
+    assert file_handler.formatter._fmt == VisorLogger.LOGGING_FORMAT
+
+def test_propagate_is_false(tmp_path):
+    """Verify that log propagation is disabled by default."""
+    logger = VisorLogger("test_logger", "logfile.log", log_dir=tmp_path)
+    assert logger.propagate is False
+
+def test_log_writes_to_file(tmp_path):
+    """Verify that log messages are written to the configured file."""
+    test_message = "Testing Visor logging!"
+    filename = "my.log"
+    logger = VisorLogger("file_logger", filename, log_dir=tmp_path)
+    logger.info(test_message)
+
+    log_file = tmp_path / filename
+    with open(log_file, "r", encoding="utf-8") as f:
+        contents = f.read()
+        assert test_message in contents
+
+def test_set_up_named_logger_creates_logger(tmp_path):
+    """Verify that set_up_named_logger creates a configured named logger."""
+    log_name = "my_named_logger"
+    log_file = "named.log"
+    logger = set_up_named_logger(log_name, log_file, tmp_path)
+
+    # Check logger properties
+    assert logger.name == log_name
+    assert logger.level == logging.DEBUG
+
+    # Ensure handler is attached with correct file path
+    file_handler = next((h for h in logger.handlers if isinstance(h, logging.FileHandler)), None)
+    assert file_handler is not None
+    assert Path(file_handler.baseFilename) == tmp_path / log_file
+    assert isinstance(file_handler.formatter, logging.Formatter)
+    assert file_handler.formatter._fmt == VisorLogger.LOGGING_FORMAT
+
+def test_configure_trame_logging_sets_up_lifecycle_logger(tmp_path):
+    """Verify that configure_trame_logging configures the Trame lifecycle logger and related loggers."""
+    logger = configure_trame_logging(str(tmp_path))
+
+    # Check that the returned logger has correct name
+    assert isinstance(logger, VisorLogger)
+    assert logger.name == "trame_lifecycle"
+    assert logger.filename == "visor_trame_app.log"
+    assert logger.log_dir == str(tmp_path)
+    assert logger.propagate is True
+
+    # Check additional loggers are configured
+    for name in ["trame", "trame_server", "trame.app"]:
+        sub_logger = logging.getLogger(name)
+        assert sub_logger.level == logging.DEBUG
+        assert any(isinstance(h, logging.FileHandler) for h in sub_logger.handlers)
+
+        handler_files = [Path(h.baseFilename).name for h in sub_logger.handlers if isinstance(h, logging.FileHandler)]
+        assert "visor_trame_app.log" in handler_files
+
+def test_logging_writes_to_file(tmp_path):
+    """Verify that a named logger writes messages to its output file."""
+    filename = "out.log"
+    log_message = "Testing logging to named logger"
+
+    logger = set_up_named_logger("temp_logger", filename, tmp_path)
+    logger.info(log_message)
+
+    log_file = tmp_path / filename
+    assert log_file.exists()
+
+    with open(log_file, encoding="utf-8") as f:
+        contents = f.read()
+        assert log_message in contents
diff --git a/tests/unit/models/test_dataset_info.py b/tests/unit/models/test_dataset_info.py
new file mode 100644
index 00000000..d5fecd26
--- /dev/null
+++ b/tests/unit/models/test_dataset_info.py
@@ -0,0 +1,30 @@
+import pytest
+
+from ansys.visor.viewer.models.info.visor_dataset_info import VisorDatasetInfo
+
+
+def test_basic_construction_and_fields():
+    """Validate basic construction and fields."""
+    ds = VisorDatasetInfo(id=1, name="Dataset 1", unit="mm")
+    assert ds.id == 1
+    assert ds.name == "Dataset 1"
+    assert ds.unit == "mm"
+    assert ds.file_path is None
+    assert ds.metadata_path is None
+
+def test_missing_required_fields_raises():
+    """Validate that missing required fields raises Exception."""
+    # missing required fields should raise a validation error
+    with pytest.raises(Exception):
+        VisorDatasetInfo(name="no id", unit="mm")
+    with pytest.raises(Exception):
+        VisorDatasetInfo(id=10, unit="mm")
+    with pytest.raises(Exception):
+        VisorDatasetInfo(id=11, name="no unit")
+
+def test_optional_file_paths_can_be_set():
+    """Validate that optional file paths can be set."""
+    ds = VisorDatasetInfo(id=5, name="WithPaths", unit="mm", file_path="/tmp/data.vtm", metadata_path="/tmp/data.json")
+    assert ds.file_path == "/tmp/data.vtm"
+    assert ds.metadata_path == "/tmp/data.json"
+
diff --git a/tests/unit/models/test_dataset_state.py b/tests/unit/models/test_dataset_state.py
new file mode 100644
index 00000000..761a25a3
--- /dev/null
+++ b/tests/unit/models/test_dataset_state.py
@@ -0,0 +1,96 @@
+from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState, RuntimePartProperties
+
+
+def test_parts_accepts_partproperties_instances():
+    """Validate that RuntimeDatasetState.part_states accepts RuntimePartProperties instances."""
+    p = RuntimePartProperties(id=123, opacity=0.75)
+    ds = RuntimeDatasetState(id=345, part_states={10: p})
+    assert 10 in ds.part_states
+    assert isinstance(ds.part_states[10], RuntimePartProperties)
+    assert ds.part_states[10].opacity == 0.75
+
+def test_parts_key_types_preserved():
+    """Validate that RuntimeDatasetState.part_states preserves key types."""
+    # ensure that integer keys remain integers (pydantic won't coerce dict keys automatically)
+    ds = RuntimeDatasetState(id=345, part_states={1: RuntimePartProperties(id=123, opacity=1.0)})
+    keys = list(ds.part_states.keys())
+    assert all(isinstance(k, int) for k in keys)
+
+
+# --- Serialization: undefined-vs-null contract ---
+
+def test_spectrum_id_null_always_serialized():
+    """spectrum_id=None must appear in the output as null (not be omitted).
+
+    Frontend distinguishes null ("remove spectrum") from absent ("pass-through").
+    The wire format uses the camelCase alias ``spectrumId``.
+    """
+    p = RuntimePartProperties(id=1, spectrum_id=None)
+    data = p.model_dump(by_alias=True)
+    assert "spectrumId" in data
+    assert data["spectrumId"] is None
+
+
+def test_spectrum_id_value_serialized():
+    """spectrum_id with a real value must be present."""
+    p = RuntimePartProperties(id=1, spectrum_id="pressure")
+    data = p.model_dump(by_alias=True)
+    assert data["spectrumId"] == "pressure"
+
+
+def test_optional_fields_omitted_when_none():
+    """opacity, visible, selected, spectrumComponent, diffuseRgb must be omitted when None.
+
+    The frontend interprets an absent key as undefined / pass-through.
+    """
+    p = RuntimePartProperties(id=1)  # all optional fields default to None
+    data = p.model_dump(by_alias=True)
+    for field in ("opacity", "visible", "selected", "spectrumComponent", "diffuseRgb"):
+        assert field not in data, f"Expected '{field}' to be omitted when None, but it was present"
+
+
+def test_optional_fields_present_when_set():
+    """Optional fields must appear when they carry an actual value."""
+    p = RuntimePartProperties(id=1, opacity=0.5, visible=True, selected=False, spectrum_component=2,
+                              diffuse_rgb=[0.1, 0.2, 0.3])
+    data = p.model_dump(by_alias=True)
+    assert data["opacity"] == 0.5
+    assert data["visible"] is True
+    assert data["selected"] is False
+    assert data["spectrumComponent"] == 2
+    assert data["diffuseRgb"] == [0.1, 0.2, 0.3]
+
+
+def test_clear_spectrum_round_trip():
+    """A part with spectrum_id=None serializes correctly and round-trips through PartProperties."""
+    from ansys.visor.viewer.models.common.part_properties import PartProperties
+
+    props = PartProperties(color_by=None)
+    p = RuntimePartProperties.from_part_properties(id=42, props=props)
+    data = p.model_dump(by_alias=True)
+
+    # spectrumId must be present and null so the frontend removes the spectrum
+    assert "spectrumId" in data
+    assert data["spectrumId"] is None
+
+    # Round-trip back to PartProperties
+    back = p.to_part_properties()
+    assert back.color_by is None
+
+
+def test_diffuse_rgb_round_trip():
+    """A part with diffuse RGB values should serialize and round-trip through PartProperties."""
+    from ansys.visor.viewer.models.common.part_properties import PartProperties
+
+    orig = [0.4, 0.5, 0.6]
+    props = PartProperties(diffuse_rgb=orig)
+    p = RuntimePartProperties.from_part_properties(id=99, props=props)
+    data = p.model_dump(by_alias=True)
+
+    # diffuseRgb must be present when set
+    assert "diffuseRgb" in data
+    assert data["diffuseRgb"] == orig
+
+    # Round-trip back to PartProperties
+    back = p.to_part_properties()
+    assert back.diffuse_rgb == orig
diff --git a/tests/unit/models/test_runtime_scene_state.py b/tests/unit/models/test_runtime_scene_state.py
new file mode 100644
index 00000000..2111120b
--- /dev/null
+++ b/tests/unit/models/test_runtime_scene_state.py
@@ -0,0 +1,114 @@
+from unittest.mock import MagicMock
+
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.models.common.visor_variable_state import (
+    VisorVariableState,
+)
+from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import (
+    RuntimeDatasetState,
+)
+from ansys.visor.viewer.models.runtime.scene.runtime_scene_state import (
+    RuntimeSceneState,
+)
+
+# ------------------------------------------------------------------
+# dataset_states validator
+# ------------------------------------------------------------------
+
+def test_dataset_states_none_becomes_empty():
+    """None dataset_states should be coerced to empty dict."""
+
+    state = RuntimeSceneState(dataset_states=None)
+
+    assert state.dataset_states == {}
+
+
+def test_dataset_states_coerce_string_keys_to_int():
+    """String keys should be coerced to int."""
+
+    data = {
+        "1": MagicMock(spec=RuntimeDatasetState),
+        "2": MagicMock(spec=RuntimeDatasetState),
+    }
+
+    state = RuntimeSceneState(dataset_states=data)
+
+    assert set(state.dataset_states.keys()) == {1, 2}
+
+
+def test_dataset_states_invalid_keys_raise():
+    """Invalid keys should fail validation."""
+
+    data = {
+        "bad": MagicMock(spec=RuntimeDatasetState),
+    }
+
+    with pytest.raises(ValidationError):
+        RuntimeSceneState(dataset_states=data)
+
+
+def test_dataset_states_passthrough_non_dict():
+    """Non-dict dataset_states should fail validation."""
+
+    with pytest.raises(ValidationError):
+        RuntimeSceneState(dataset_states=123)
+
+
+# ------------------------------------------------------------------
+# general model behavior
+# ------------------------------------------------------------------
+
+def test_default_fields():
+    """Default construction should initialize expected defaults."""
+
+    state = RuntimeSceneState()
+
+    assert state.dataset_states == {}
+    assert state.spectrum_states == {}
+    assert state.unit is None
+
+
+def test_setting_basic_fields():
+    """Basic fields should be stored correctly."""
+
+    state = RuntimeSceneState(
+        unit="m",
+        orthographic_enabled=True,
+        edges_enabled=False,
+    )
+
+    assert state.unit == "m"
+    assert state.orthographic_enabled is True
+    assert state.edges_enabled is False
+
+
+def test_dataset_states_accept_valid_mapping():
+    """Valid dataset_states should be accepted."""
+
+    data = {
+        "1": MagicMock(spec=RuntimeDatasetState),
+    }
+
+    state = RuntimeSceneState(dataset_states=data)
+
+    assert 1 in state.dataset_states
+    assert isinstance(state.dataset_states[1], RuntimeDatasetState)
+
+
+# ------------------------------------------------------------------
+# spectrum_states
+# ------------------------------------------------------------------
+
+def test_spectrum_states_default_and_assignment():
+    """spectrum_states should accept valid mapping."""
+
+    data = {
+        "a": MagicMock(spec=VisorVariableState),
+    }
+
+    state = RuntimeSceneState(spectrum_states=data)
+
+    assert "a" in state.spectrum_states
+    assert isinstance(state.spectrum_states["a"], VisorVariableState)
diff --git a/tests/unit/models/test_visor_camera_state.py b/tests/unit/models/test_visor_camera_state.py
new file mode 100644
index 00000000..12607ef9
--- /dev/null
+++ b/tests/unit/models/test_visor_camera_state.py
@@ -0,0 +1,77 @@
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState
+
+
+def test_valid_camera_state():
+    """Verify that a valid VisorCameraState is created successfully."""
+    cam = VisorCameraState(
+        position=[1.0, 2.0, 3.0],
+        focal_point=[0.0, 0.0, 0.0],
+        view_up=[0.0, 1.0, 0.0],
+        clipping_range=[0.1, 1000.0],
+        parallel_projection=True,
+        view_angle=30.0,
+        parallel_scale=1.5,
+    )
+
+    assert cam.position == [1.0, 2.0, 3.0]
+    assert cam.focal_point == [0.0, 0.0, 0.0]
+    assert cam.view_up == [0.0, 1.0, 0.0]
+    assert cam.clipping_range == [0.1, 1000.0]
+    assert cam.parallel_projection is True
+    assert isinstance(cam.view_angle, float) and cam.view_angle == 30.0
+    assert isinstance(cam.parallel_scale, float) and cam.parallel_scale == 1.5
+
+
+def test_missing_required_field_raises():
+    """Verify that missing required field raises ValidationError."""
+    # omit 'position' which is required
+    with pytest.raises(ValidationError):
+        VisorCameraState(
+            focal_point=[0.0, 0.0, 0.0],
+            view_up=[0.0, 1.0, 0.0],
+            clipping_range=[0.1, 1000.0],
+            parallel_projection=False,
+            view_angle=45.0,
+            parallel_scale=2.0,
+        )
+
+
+def test_invalid_element_type_in_list_raises():
+    """Verify that invalid element type raises ValidationError."""
+    # a non-float element in the position list should cause validation to fail
+    with pytest.raises(ValidationError):
+        VisorCameraState(
+            position=[1.0, 'bad', 3.0],
+            focal_point=[0.0, 0.0, 0.0],
+            view_up=[0.0, 1.0, 0.0],
+            clipping_range=[0.1, 1000.0],
+            parallel_projection=False,
+            view_angle=45.0,
+            parallel_scale=2.0,
+        )
+
+
+def test_type_coercion_from_tuples_and_ints():
+    """Verify that pydantic coerces tuples to lists and ints to floats where appropriate."""
+    # pydantic should coerce tuples to lists and ints to floats where appropriate
+    cam = VisorCameraState(
+        position=(1, 2, 3),
+        focal_point=(0, 0, 0),
+        view_up=(0, 1, 0),
+        clipping_range=(1, 200),
+        parallel_projection=0,  # falsy int should be accepted as bool-ish
+        view_angle=60,  # int -> float
+        parallel_scale=4,  # int -> float
+    )
+
+    assert cam.position == [1, 2, 3]
+    assert cam.focal_point == [0, 0, 0]
+    assert cam.view_up == [0, 1, 0]
+    assert cam.clipping_range == [1, 200]
+    # pydantic will coerce 0 to False for a bool field
+    assert cam.parallel_projection is False
+    assert isinstance(cam.view_angle, float) and cam.view_angle == 60.0
+    assert isinstance(cam.parallel_scale, float) and cam.parallel_scale == 4.0
diff --git a/tests/unit/models/test_visor_save_state_request.py b/tests/unit/models/test_visor_save_state_request.py
new file mode 100644
index 00000000..2d11b171
--- /dev/null
+++ b/tests/unit/models/test_visor_save_state_request.py
@@ -0,0 +1,50 @@
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.models.runtime.requests.visor_save_state_request import (
+    VisorSaveStateRequest,
+)
+
+
+def test_create_valid_request():
+    """Valid request should be created with request_id."""
+
+    req = VisorSaveStateRequest(request_id=123)
+
+    assert req.request_id == 123
+
+
+def test_request_id_is_required():
+    """Missing request_id should raise ValidationError."""
+
+    with pytest.raises(ValidationError):
+        VisorSaveStateRequest()
+
+
+def test_request_id_must_be_int():
+    """Non-integer request_id should raise ValidationError."""
+
+    with pytest.raises(ValidationError):
+        VisorSaveStateRequest(request_id="not-an-int")
+
+
+def test_model_dump_contains_request_id():
+    """model_dump(by_alias=True) should include the camelCase requestId wire field."""
+
+    req = VisorSaveStateRequest(request_id=5)
+    data = req.model_dump(by_alias=True)
+
+    assert data == {"requestId": 5}
+
+
+def test_arbitrary_types_allowed_behavior():
+    """Model should allow arbitrary types without failure."""
+
+    class Dummy:
+        pass
+
+    # Nothing in the schema uses arbitrary types directly,
+    # but Config enables it โ€” this ensures it doesn't break.
+    req = VisorSaveStateRequest(request_id=1)
+
+    assert isinstance(req, VisorSaveStateRequest)
diff --git a/tests/unit/models/test_visor_save_state_response.py b/tests/unit/models/test_visor_save_state_response.py
new file mode 100644
index 00000000..9c4b9983
--- /dev/null
+++ b/tests/unit/models/test_visor_save_state_response.py
@@ -0,0 +1,114 @@
+import json
+from unittest.mock import MagicMock
+
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.models.runtime.requests.visor_save_state_response import (
+    VisorSaveStateResponse,
+)
+from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState
+
+# ------------------------------------------------------------------
+# Helpers
+# ------------------------------------------------------------------
+
+def make_runtime_state():
+    """Create a RuntimeAppState-compatible mock."""
+    return MagicMock(spec=RuntimeAppState)
+
+
+# ------------------------------------------------------------------
+# Tests
+# ------------------------------------------------------------------
+
+def test_create_response_from_dict(monkeypatch):
+    """Dict app_state should be converted via model_validate."""
+
+    monkeypatch.setattr(
+        RuntimeAppState,
+        "model_validate",
+        lambda data: make_runtime_state(),
+    )
+
+    resp = VisorSaveStateResponse(request_id=2, app_state={"x": 10})
+
+    assert isinstance(resp.app_state, RuntimeAppState)
+
+
+def test_create_response_from_json_string(monkeypatch):
+    """JSON string app_state should be parsed and validated."""
+
+    monkeypatch.setattr(
+        RuntimeAppState,
+        "model_validate",
+        lambda data: make_runtime_state(),
+    )
+
+    data = json.dumps({"foo": "bar"})
+
+    resp = VisorSaveStateResponse(request_id=3, app_state=data)
+
+    assert isinstance(resp.app_state, RuntimeAppState)
+
+
+def test_create_valid_response_with_model_passthrough():
+    """Already-constructed RuntimeAppState should pass through unchanged."""
+
+    existing = make_runtime_state()
+
+    resp = VisorSaveStateResponse(request_id=1, app_state=existing)
+
+    assert resp.app_state is existing
+
+
+def test_invalid_request_id_raises():
+    """Non-integer request_id should raise ValidationError."""
+
+    with pytest.raises(ValidationError):
+        VisorSaveStateResponse(request_id="bad", app_state={})
+
+
+def test_missing_app_state_raises():
+    """Missing app_state should raise ValidationError."""
+
+    with pytest.raises(ValidationError):
+        VisorSaveStateResponse(request_id=1)
+
+
+def test_invalid_json_string_raises(monkeypatch):
+    """Invalid JSON string should raise during parsing."""
+
+    monkeypatch.setattr(
+        RuntimeAppState,
+        "model_validate",
+        lambda data: make_runtime_state(),
+    )
+
+    with pytest.raises(Exception):
+        VisorSaveStateResponse(request_id=1, app_state="not-json")
+
+
+def test_invalid_type_for_app_state_raises():
+    """Unexpected app_state type should fail validation."""
+
+    with pytest.raises(ValidationError):
+        VisorSaveStateResponse(request_id=1, app_state=123)
+
+
+def test_model_dump_contains_fields(monkeypatch):
+    """model_dump(by_alias=True) should include the expected camelCase wire fields."""
+
+    dummy = make_runtime_state()
+
+    monkeypatch.setattr(
+        RuntimeAppState,
+        "model_validate",
+        lambda data: dummy,
+    )
+
+    resp = VisorSaveStateResponse(request_id=5, app_state={"k": "v"})
+    data = resp.model_dump(by_alias=True)
+
+    assert data["requestId"] == 5
+    assert "appState" in data
diff --git a/tests/unit/models/test_visor_state.py b/tests/unit/models/test_visor_state.py
new file mode 100644
index 00000000..2ded7b0f
--- /dev/null
+++ b/tests/unit/models/test_visor_state.py
@@ -0,0 +1,39 @@
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.models.common.part_properties import PartProperties
+from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState
+
+
+def test_partproperties_valid_opacity():
+    """Verify that PartProperties.opacity can be set correctly."""
+    p = PartProperties(opacity=0.5)
+    assert p.opacity == 0.5
+
+def test_partproperties_none_opacity():
+    """Verify that PartProperties.opacity can be None."""
+    p = PartProperties()
+    assert p.opacity is None
+
+@pytest.mark.parametrize("value", [-0.1, 1.1])
+def test_partproperties_invalid_opacity(value):
+    """Verify that PartProperties.opacity raises ValidationError for invalid values."""
+    with pytest.raises(ValidationError):
+        PartProperties(opacity=value)
+
+def test_visorstate_empty_parts():
+    """Verify that RuntimeDatasetState can be created with empty parts."""
+    s = RuntimeDatasetState(id=1)
+    assert s.part_states == {}
+
+def test_visorstate_parts_with_partproperties():
+    """Verify that RuntimeDatasetState can be created with PartProperties."""
+    p = PartProperties(opacity=0.7)
+    ds = RuntimeDatasetState.from_components(id=5, part_states={10: p})
+    assert 10 in ds.part_states
+    # The runtime representation should preserve the opacity value
+    assert ds.part_states[10].opacity == 0.7
+    # Ensure diffuse fields round-trip when present
+    p2 = PartProperties(diffuse_rgb=[0.2, 0.3, 0.4])
+    ds2 = RuntimeDatasetState.from_components(id=6, part_states={11: p2})
+    assert ds2.part_states[11].diffuse_rgb == [0.2, 0.3, 0.4]
diff --git a/tests/unit/renderer/__init__.py b/tests/unit/renderer/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/renderer/test_local_renderer.py b/tests/unit/renderer/test_local_renderer.py
new file mode 100644
index 00000000..c0bd33ae
--- /dev/null
+++ b/tests/unit/renderer/test_local_renderer.py
@@ -0,0 +1,482 @@
+"""
+Unit tests for IRenderer / VisorLocalRenderer.
+
+Coverage targets
+----------------
+1.  Node lifecycle  -- register_node / deregister_node (the non-trivial seam).
+2.  Interface conformance -- NullRenderer and VisorLocalRenderer both satisfy
+    IRenderer's abstract contract.
+3.  Per-part visual mutations -- one focused test per property;
+    tests assert they accept their arguments and return None.
+4.  Camera round-trip -- reset_camera, sync_camera / get_camera_state.
+5.  Render / flush delegation.
+6.  pick_geometry -- vertex, edge, face modes.
+"""
+
+from __future__ import annotations
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer.renderer.base import IRenderer
+from ansys.visor.viewer.renderer.local_renderer import VisorLocalRenderer
+from ansys.visor.viewer.renderer.null_renderer import NullRenderer
+
+# ---------------------------------------------------------------------------
+# Lightweight VTK-data stubs (no real VTK objects required for pick tests)
+# ---------------------------------------------------------------------------
+
+class _DummyPoints:
+    def __init__(self, pts):
+        self._pts = pts
+
+    def GetPoint(self, i): # noqa: N802
+        return tuple(self._pts[i])
+
+
+class _DummyCell:
+    def __init__(self, points):
+        self._pts = _DummyPoints(points)
+        self._n = len(points)
+
+    def GetNumberOfPoints(self): # noqa: N802
+        return self._n
+
+    def GetPoints(self): # noqa: N802
+        return self._pts
+
+
+class _DummyDataset:
+    def __init__(self, cells):
+        self._cells = cells
+
+    def GetNumberOfCells(self): # noqa: N802
+        return len(self._cells)
+
+    def GetCell(self, idx): # noqa: N802
+        return self._cells[idx] if 0 <= idx < len(self._cells) else None
+
+
+class _DummyMapper:
+    def __init__(self, dataset):
+        self._ds = dataset
+
+    def GetInputDataObject(self, a, b): # noqa: N802
+        return self._ds
+
+
+class _DummyActor:
+    def __init__(self, mapper):
+        self._mapper = mapper
+
+    def GetMapper(self): # noqa: N802
+        return self._mapper
+
+
+class _DummyActors:
+    """Minimal traversable actor collection matching vtkActorCollection's API."""
+
+    def __init__(self, actors):
+        self._actors = actors
+        self._idx = 0
+
+    def InitTraversal(self): # noqa: N802
+        self._idx = 0
+
+    def GetNextActor(self): # noqa: N802
+        if self._idx >= len(self._actors):
+            return None
+        a = self._actors[self._idx]
+        self._idx += 1
+        return a
+
+
+# ---------------------------------------------------------------------------
+# Fixture: VisorLocalRenderer with all VTK infrastructure mocked
+# ---------------------------------------------------------------------------
+
+@pytest.fixture
+def renderer():
+    """VisorLocalRenderer whose every VTK sub-system is a MagicMock.
+
+    Each ``_initialize_*`` method is patched so that no real VTK render window,
+    interactor, LocalView, or widget is created.  The resulting object has fully
+    controllable mock attributes (``_vtk_renderer``, ``_cross_section_widget``,
+    etc.) that each test can configure independently.
+    """
+    mock_server = MagicMock()
+    mock_server.state = {}
+
+    vtk_renderer = MagicMock(name="vtk_renderer")
+    render_window = MagicMock(name="render_window")
+    interactor = MagicMock(name="interactor")
+    local_view = MagicMock(name="local_view")
+    orientation_widget = MagicMock(name="orientation_widget")
+    cross_section_widget = MagicMock(name="cross_section_widget")
+    bounding_box_widget = MagicMock(name="bounding_box_widget")
+
+    with (
+        patch.object(VisorLocalRenderer, "_initialize_vtk_renderer", return_value=vtk_renderer),
+        patch.object(VisorLocalRenderer, "_initialize_render_window", return_value=render_window),
+        patch.object(VisorLocalRenderer, "_initialize_render_window_interactor", return_value=interactor),
+        patch.object(VisorLocalRenderer, "_initialize_local_view", return_value=local_view),
+        patch.object(VisorLocalRenderer, "_initialize_orientation_widget", return_value=orientation_widget),
+        patch.object(VisorLocalRenderer, "_initialize_cross_section_widget", return_value=cross_section_widget),
+        patch.object(VisorLocalRenderer, "_initialize_bounding_box_widget", return_value=bounding_box_widget),
+    ):
+        r = VisorLocalRenderer(mock_server)
+
+    return r
+
+
+# ---------------------------------------------------------------------------
+# Helper: seed one actor into the renderer's actor list for pick tests
+# ---------------------------------------------------------------------------
+
+def _seed_actor(renderer, actor_wasm_id: int, dataset):
+    """Wire a dummy actor+dataset into the renderer's VTK actor traversal.
+
+    Returns the actor so callers can make additional assertions.
+    """
+    actor = _DummyActor(_DummyMapper(dataset))
+    renderer._vtk_renderer.GetActors.return_value = _DummyActors([actor])
+    renderer._object_manager.GetId.side_effect = (
+        lambda a: actor_wasm_id if a is actor else -1
+    )
+    return actor
+
+
+# ===========================================================================
+# 1.  Interface conformance
+# ===========================================================================
+
+class TestInterfaceConformance:
+    def test_null_renderer_instantiates_without_type_error(self):
+        """NullRenderer satisfies IRenderer's full abstract set."""
+        r = NullRenderer()
+        assert isinstance(r, IRenderer)
+
+    def test_local_renderer_has_no_remaining_abstract_methods(self):
+        """VisorLocalRenderer provides a concrete body for every @abstractmethod."""
+        assert not VisorLocalRenderer.__abstractmethods__
+
+    def test_null_renderer_has_no_remaining_abstract_methods(self):
+        assert not NullRenderer.__abstractmethods__
+
+
+# ===========================================================================
+# 2.  Node lifecycle  (register_node / deregister_node)
+# ===========================================================================
+
+class TestNodeLifecycle:
+
+    # ------------------------------------------------------------------
+    # register_node
+    # ------------------------------------------------------------------
+
+    def test_register_node_calls_from_dataset_with_node_dataset(self, renderer):
+        """register_node builds the pipeline from the node's dataset."""
+        node = MagicMock()
+        node.id = 10
+        dataset = MagicMock(name="dataset")
+        mock_pipe = MagicMock(name="pipeline")
+
+        with patch(
+            "ansys.visor.viewer.renderer.local_renderer.VtkNodePipeline.from_dataset",
+            return_value=mock_pipe,
+        ) as mock_from_ds:
+            renderer.register_node(node, dataset)
+
+        mock_from_ds.assert_called_once_with(dataset)
+
+    def test_register_node_stores_pipeline_keyed_by_node_id(self, renderer):
+        """Pipeline is registered under node.id in the internal registry."""
+        node = MagicMock()
+        node.id = 99
+        mock_pipe = MagicMock(name="pipeline")
+
+        with patch(
+            "ansys.visor.viewer.renderer.local_renderer.VtkNodePipeline.from_dataset",
+            return_value=mock_pipe,
+        ):
+            renderer.register_node(node, MagicMock())
+
+        assert renderer._pipelines[99] is mock_pipe
+
+    def test_register_node_attaches_cross_section_clipping_plane(self, renderer):
+        """Pipeline's clipping plane is set to the cross-section widget's plane."""
+        node = MagicMock()
+        node.id = 7
+        mock_pipe = MagicMock(name="pipeline")
+
+        with patch(
+            "ansys.visor.viewer.renderer.local_renderer.VtkNodePipeline.from_dataset",
+            return_value=mock_pipe,
+        ):
+            renderer.register_node(node, MagicMock())
+
+        mock_pipe.set_clipping_plane.assert_called_once_with(
+            renderer._cross_section_widget.plane
+        )
+
+    def test_register_node_disables_scalar_visibility(self, renderer):
+        """Mapper scalar visibility is turned off immediately after pipeline creation."""
+        node = MagicMock()
+        node.id = 5
+        mock_pipe = MagicMock(name="pipeline")
+
+        with patch(
+            "ansys.visor.viewer.renderer.local_renderer.VtkNodePipeline.from_dataset",
+            return_value=mock_pipe,
+        ):
+            renderer.register_node(node, MagicMock())
+
+        mock_pipe.mapper.SetScalarVisibility.assert_called_once_with(False)
+
+    def test_register_node_adds_actor_to_vtk_renderer(self, renderer):
+        """Actor is added to the VTK renderer so it appears in the scene."""
+        node = MagicMock()
+        node.id = 3
+        mock_pipe = MagicMock(name="pipeline")
+
+        with patch(
+            "ansys.visor.viewer.renderer.local_renderer.VtkNodePipeline.from_dataset",
+            return_value=mock_pipe,
+        ):
+            renderer.register_node(node, MagicMock())
+
+        renderer._vtk_renderer.AddActor.assert_called_once_with(mock_pipe.actor)
+
+    def test_register_node_is_idempotent(self, renderer):
+        """Calling register_node twice for the same node does not create a duplicate."""
+        node = MagicMock()
+        node.id = 1
+        mock_pipe = MagicMock(name="pipeline")
+
+        with patch(
+            "ansys.visor.viewer.renderer.local_renderer.VtkNodePipeline.from_dataset",
+            return_value=mock_pipe,
+        ) as mock_from_ds:
+            renderer.register_node(node, MagicMock())
+            renderer.register_node(node, MagicMock())  # second call -- must be ignored
+
+        # Pipeline factory called only once; registry still has exactly one entry.
+        mock_from_ds.assert_called_once()
+        assert len(renderer._pipelines) == 1
+        # Actor attached only once.
+        renderer._vtk_renderer.AddActor.assert_called_once()
+
+    # ------------------------------------------------------------------
+    # deregister_node
+    # ------------------------------------------------------------------
+
+    def test_deregister_node_removes_actor_from_vtk_renderer(self, renderer):
+        """deregister_node removes the actor from the VTK renderer."""
+        node_id = 20
+        mock_pipe = MagicMock(name="pipeline")
+        renderer._pipelines[node_id] = mock_pipe
+
+        renderer.deregister_node(node_id)
+
+        renderer._vtk_renderer.RemoveActor.assert_called_once_with(mock_pipe.actor)
+
+    def test_deregister_node_removes_pipeline_from_registry(self, renderer):
+        """Pipeline is gone from the registry after deregistration."""
+        node_id = 21
+        renderer._pipelines[node_id] = MagicMock(name="pipeline")
+
+        renderer.deregister_node(node_id)
+
+        assert node_id not in renderer._pipelines
+
+    def test_deregister_node_silently_ignores_unknown_id(self, renderer):
+        """deregister_node is a no-op when the node was never registered."""
+        renderer.deregister_node(9999)  # must not raise
+        renderer._vtk_renderer.RemoveActor.assert_not_called()
+
+    def test_register_then_deregister_leaves_empty_registry(self, renderer):
+        """Full lifecycle: register one node then deregister it; registry is empty."""
+        node = MagicMock()
+        node.id = 55
+        mock_pipe = MagicMock(name="pipeline")
+
+        with patch(
+            "ansys.visor.viewer.renderer.local_renderer.VtkNodePipeline.from_dataset",
+            return_value=mock_pipe,
+        ):
+            renderer.register_node(node, MagicMock())
+
+        renderer.deregister_node(55)
+
+        assert renderer._pipelines == {}
+        renderer._vtk_renderer.RemoveActor.assert_called_once_with(mock_pipe.actor)
+
+    # ------------------------------------------------------------------
+    # deregister_all
+    # ------------------------------------------------------------------
+
+    def test_deregister_all_empties_registry_and_detaches_all_actors(self, renderer):
+        """deregister_all removes every pipeline and detaches every actor."""
+        mock_pipe_1 = MagicMock(name="pipeline1")
+        mock_pipe_2 = MagicMock(name="pipeline2")
+        renderer._pipelines[1] = mock_pipe_1
+        renderer._pipelines[2] = mock_pipe_2
+
+        renderer.deregister_all()
+
+        assert renderer._pipelines == {}
+        renderer._vtk_renderer.RemoveActor.assert_any_call(mock_pipe_1.actor)
+        renderer._vtk_renderer.RemoveActor.assert_any_call(mock_pipe_2.actor)
+        assert renderer._vtk_renderer.RemoveActor.call_count == 2
+
+
+# ===========================================================================
+# 3.  Per-part visual mutations
+# ===========================================================================
+
+class TestPerPartMutations:
+    """Each method accepts its contract arguments and returns None without raising."""
+
+    def test_apply_visibility(self, renderer):
+        assert renderer.apply_visibility(1, True) is None
+
+    def test_apply_opacity(self, renderer):
+        assert renderer.apply_opacity(1, 0.5) is None
+
+    def test_apply_diffuse_color(self, renderer):
+        assert renderer.apply_diffuse_color(1, 1.0, 0.0, 0.0) is None
+
+    def test_apply_edge_visibility(self, renderer):
+        assert renderer.apply_edge_visibility(1, False) is None
+
+    def test_apply_selected(self, renderer):
+        assert renderer.apply_selected(1, True, [1.0, 0.0, 0.0]) is None
+
+    def test_apply_color_variable(self, renderer):
+        assert (
+            renderer.apply_color_variable(1, "sp-1", "POINT", "pressure", -1, 0.0, 1.0)
+            is None
+        )
+
+    def test_clear_color_variable(self, renderer):
+        assert renderer.clear_color_variable(1) is None
+
+    def test_refresh_color_variable_range(self, renderer):
+        assert (
+            renderer.refresh_color_variable_range(1, "sp-1", "CELL", "temp", 0)
+            is None
+        )
+
+
+# ===========================================================================
+# 4.  Camera
+# ===========================================================================
+
+class TestCamera:
+    def test_reset_camera_delegates_to_vtk_renderer(self, renderer):
+        bounds = [0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
+        renderer.reset_camera(bounds)
+        renderer._vtk_renderer.ResetCamera.assert_called_once_with(bounds)
+
+    def test_get_camera_state_returns_none_initially(self, renderer):
+        assert renderer.get_camera_state() is None
+
+    def test_sync_camera_stores_state_for_get(self, renderer):
+        cam = MagicMock(name="camera_state")
+        renderer.sync_camera(cam)
+        assert renderer.get_camera_state() is cam
+
+
+# ===========================================================================
+# 5.  Render / flush delegation
+# ===========================================================================
+
+class TestRenderFlush:
+    def test_render_calls_render_window_then_local_view_update(self, renderer):
+        renderer.render()
+        renderer._render_window.Render.assert_called_once()
+        renderer._local_view.update.assert_called_once()
+
+    def test_render_window_only_calls_render_window_not_local_view(self, renderer):
+        renderer.render_window_only()
+        renderer._render_window.Render.assert_called_once()
+        renderer._local_view.update.assert_not_called()
+
+    def test_flush_wasm_state_calls_local_view_update(self, renderer):
+        """VisorLocalRenderer overrides the base no-op to push state to wasm."""
+        renderer.flush_wasm_state()
+        renderer._local_view.update.assert_called_once()
+
+
+
+class TestPickGeometry:
+
+    def test_pick_vertex_returns_nearest_point(self, renderer):
+        """Vertex mode returns the cell point closest to the pick position."""
+        pts = [(0, 0, 0), (1, 0, 0), (0, 1, 0)]
+        dataset = _DummyDataset([_DummyCell(pts)])
+        _seed_actor(renderer, 123, dataset)
+
+        res = renderer.pick_geometry(123, 0, "vertex", (0.9, 0.1, 0.0))
+
+        assert res["found"] is True
+        assert res["mode"] == "vertex"
+        assert tuple(res["position"]) == (1.0, 0.0, 0.0)
+
+    def test_pick_edge_returns_closest_edge_endpoints_and_length(self, renderer):
+        """Edge mode returns the two endpoints of the edge nearest to the pick."""
+        pts = [(0, 0, 0), (2, 0, 0), (2, 2, 0), (0, 2, 0)]
+        dataset = _DummyDataset([_DummyCell(pts)])
+        _seed_actor(renderer, 7, dataset)
+
+        res = renderer.pick_geometry(7, 0, "edge", (2.01, 1.0, 0.0))
+
+        assert res["found"] is True
+        assert res["mode"] == "edge"
+        assert tuple(res["pointA"]) == (2.0, 0.0, 0.0)
+        assert tuple(res["pointB"]) == (2.0, 2.0, 0.0)
+        assert pytest.approx(res["length"]) == 2.0
+
+    def test_pick_face_returns_area_and_all_points(self, renderer):
+        """Face mode returns the polygon area and every cell point."""
+        pts = [(0, 0, 0), (1, 0, 0), (1, 2, 0), (0, 2, 0)]
+        dataset = _DummyDataset([_DummyCell(pts)])
+        _seed_actor(renderer, 5, dataset)
+
+        res = renderer.pick_geometry(5, 0, "face", (0.5, 1.0, 0.0))
+
+        assert res["found"] is True
+        assert res["mode"] == "face"
+        assert pytest.approx(res["area"]) == 2.0
+        assert len(res["points"]) == 4
+
+    def test_pick_negative_actor_id_returns_not_found(self, renderer):
+        res = renderer.pick_geometry(-1, 0, "vertex", (0.0, 0.0, 0.0))
+        assert res == {"found": False}
+
+    def test_pick_negative_cell_id_returns_not_found(self, renderer):
+        res = renderer.pick_geometry(1, -1, "vertex", (0.0, 0.0, 0.0))
+        assert res == {"found": False}
+
+    def test_pick_unregistered_actor_returns_not_found(self, renderer):
+        """If no actor matches the given wasm id, pick returns not-found."""
+        renderer._vtk_renderer.GetActors.return_value = _DummyActors([])
+        res = renderer.pick_geometry(42, 0, "vertex", (0.0, 0.0, 0.0))
+        assert res["found"] is False
+
+    def test_pick_cell_id_out_of_range_returns_not_found(self, renderer):
+        """Cell index beyond dataset size returns not-found."""
+        dataset = _DummyDataset([_DummyCell([(0, 0, 0), (1, 0, 0), (0, 1, 0)])])
+        _seed_actor(renderer, 10, dataset)
+        res = renderer.pick_geometry(10, 99, "vertex", (0.0, 0.0, 0.0))
+        assert res["found"] is False
+
+    def test_pick_unknown_mode_returns_not_found(self, renderer):
+        """An unrecognised mode string falls through to not-found."""
+        pts = [(0, 0, 0), (1, 0, 0), (0, 1, 0)]
+        dataset = _DummyDataset([_DummyCell(pts)])
+        _seed_actor(renderer, 1, dataset)
+        res = renderer.pick_geometry(1, 0, "INVALID_MODE", (0.0, 0.0, 0.0))
+        assert res == {"found": False}
+
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
new file mode 100644
index 00000000..9fbc0a13
--- /dev/null
+++ b/tests/unit/test_config.py
@@ -0,0 +1,68 @@
+from unittest.mock import patch
+
+import ansys.visor.viewer.config as config
+
+
+def test_settings_defaults():
+    """Verify that settings defaults work."""
+    s = config.Settings()
+    assert s.app_name == "Visor Viewer"
+    assert s.default_host == "localhost"
+    assert s.default_port == 8081
+    assert s.default_standalone is True
+    assert "client_bundle" in s.default_client_bundle
+    assert "logs" in s.default_log_dir
+    assert s.trame_log_dir is None
+
+def test_get_client_bundle_standalone_true():
+    """Verify that getting the client bundle works when standalone is True."""
+    s = config.Settings()
+    result = s.get_client_bundle(standalone=True)
+    assert result == s.default_client_bundle
+
+def test_get_client_bundle_standalone_false():
+    """Verify that getting client bundle is None when standalone is False."""
+    s = config.Settings()
+    result = s.get_client_bundle(standalone=False)
+    assert result is None
+
+def test_settings_customise_sources_file_exists(tmp_path, monkeypatch):
+    """Verify that .visor custom settings are loaded and certain keys are removed."""
+    # Create a fake .visor file in a temporary directory
+    config_file = tmp_path / ".visor"
+    config_file.write_text("irrelevant")
+
+    # Patch cwd so Settings looks in our tmp directory
+    monkeypatch.chdir(tmp_path)
+
+    # Fake YAML content returned by yaml.safe_load
+    fake_yaml = {
+        "app_name": "ShouldBeRemoved",
+        "default_client_bundle": "ShouldBeRemoved",
+        "default_host": "customhost",
+    }
+
+    with patch("yaml.safe_load", return_value=fake_yaml):
+        sources = config.Settings.settings_customise_sources(
+            None, "init", "env", "file", foo="bar"
+        )
+        file_settings_func = sources[0]
+        result = file_settings_func()
+
+    # Assertions
+    assert "app_name" not in result
+    assert "default_client_bundle" not in result
+    assert result["default_host"] == "customhost"
+
+def test_settings_customise_sources_file_not_exists(tmp_path, monkeypatch):
+    """Verify that .visor custom settings are not loaded when the file does not exist."""
+    monkeypatch.chdir(tmp_path)
+    sources = config.Settings.settings_customise_sources(None, "init", "env", "file", foo="bar")
+    file_settings_func = sources[0]
+    result = file_settings_func()
+    assert result == {}
+
+
+def test_settings_instance_exists():
+    """Verify that the global settings object is an instance of Settings"""
+    assert isinstance(config.settings, config.Settings)
diff --git a/tests/unit/test_init_endpoints.py b/tests/unit/test_init_endpoints.py
new file mode 100644
index 00000000..8421f22b
--- /dev/null
+++ b/tests/unit/test_init_endpoints.py
@@ -0,0 +1,218 @@
+# ยฉ2026, ANSYS Inc part of Synopsys. Unauthorized use, distribution or duplication is prohibited.
+"""
+Unit tests for the two new features added to visordash.init_endpoints():
+
+1.  DashProxy compatibility  โ€“ any object with a `server` Flask attribute
+    should be accepted, not just plain ``dash.Dash`` instances.
+
+2.  Prefix-aware asset routing โ€“ when ``base_path="/my-prefix"`` is passed,
+    every asset endpoint must be reachable under that prefix, and the built
+    CSS bundle must have its internal ``url("/fonts/โ€ฆ")`` references rewritten
+    to ``url("/my-prefix/fonts/โ€ฆ")``.
+
+The tests use Flask's built-in test client so no real browser or Visor server
+is required.
+"""
+
+import re
+
+import pytest
+from dash import Dash
+from flask import Flask
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+class _FakeDashProxy:
+    """Minimal stand-in for dash_extensions.enrich.DashProxy.
+
+    It only needs a ``.server`` attribute (a Flask app) to satisfy the new
+    duck-typing check in init_endpoints().
+    """
+
+    def __init__(self):
+        self.server = Flask(__name__ + "_fake_proxy")
+
+    def route(self, *args, **kwargs):
+        """Delegate route registration to the underlying Flask app."""
+        return self.server.route(*args, **kwargs)
+
+
+def _make_dash_app(name: str = __name__) -> Dash:
+    """Return a plain Dash app with a unique name and a minimal layout."""
+    from dash import html
+    app = Dash(name)
+    app.layout = html.Div()   # Dash requires a layout before it will serve any route
+    return app
+
+
+# ---------------------------------------------------------------------------
+# Fixtures
+# ---------------------------------------------------------------------------
+
+@pytest.fixture(autouse=True)
+def _reset_endpoints_flag():
+    """Each test gets a clean slate: reset the class-level flag on Visordash."""
+    from visordash import Visordash
+    Visordash.endpoints_set = False
+    yield
+    Visordash.endpoints_set = False
+
+
+# ---------------------------------------------------------------------------
+# 1.  DashProxy compatibility
+# ---------------------------------------------------------------------------
+
+class TestDashProxyCompatibility:
+
+    def test_plain_dash_is_accepted(self):
+        """init_endpoints should still work with a plain Dash instance."""
+        from visordash import init_endpoints
+        app = _make_dash_app("plain_dash_test")
+        # Should not raise
+        init_endpoints(app)
+
+    def test_dashproxy_lookalike_is_accepted(self):
+        """Any object with a `.server` Flask attribute must be accepted."""
+        from visordash import init_endpoints
+        proxy = _FakeDashProxy()
+        # Should not raise
+        init_endpoints(proxy)
+
+    def test_invalid_object_raises(self):
+        """An arbitrary object without a `.server` attribute must raise."""
+        from visordash import init_endpoints
+
+        class NotADash:
+            pass
+
+        with pytest.raises(Exception, match="dash_app must be"):
+            init_endpoints(NotADash())
+
+    def test_calling_init_endpoints_twice_raises(self):
+        """Calling init_endpoints a second time should raise."""
+        from visordash import init_endpoints
+        app = _make_dash_app("double_init_test")
+        init_endpoints(app)
+        with pytest.raises(Exception, match="already been set"):
+            init_endpoints(app)
+
+
+# ---------------------------------------------------------------------------
+# 2.  Prefix-aware asset routing โ€“ route registration
+# ---------------------------------------------------------------------------
+
+class TestPrefixRouteRegistration:
+    """Verify that the Flask routes are registered under the given prefix."""
+
+    ASSETS = [
+        "/visordash/js.js",
+        "/visordash/css.css",
+        "/css/theme-dark.css",
+        "/css/theme-light.css",
+        "/fonts/source-sans-3.woff2",
+    ]
+
+    def _registered_routes(self, flask_app: Flask) -> set:
+        return {rule.rule for rule in flask_app.url_map.iter_rules()}
+
+    def test_no_prefix_registers_default_routes(self):
+        """With base_path='', routes must match the original absolute paths."""
+        from visordash import init_endpoints
+        app = _make_dash_app("no_prefix_test")
+        init_endpoints(app)
+        routes = self._registered_routes(app.server)
+        for asset in self.ASSETS:
+            assert asset in routes, f"Expected route '{asset}' not found"
+
+    def test_prefix_routes_are_registered(self):
+        """With base_path='/pfx', every asset route must start with /pfx."""
+        from visordash import Visordash, init_endpoints
+        Visordash.endpoints_set = False
+        app = _make_dash_app("prefix_test")
+        init_endpoints(app, base_path="/pfx")
+        routes = self._registered_routes(app.server)
+        for asset in self.ASSETS:
+            expected = "/pfx" + asset
+            assert expected in routes, f"Expected route '{expected}' not found"
+
+    def test_prefix_routes_do_not_include_unprefixed(self):
+        """With a prefix set, the un-prefixed routes must NOT be registered."""
+        from visordash import Visordash, init_endpoints
+        Visordash.endpoints_set = False
+        app = _make_dash_app("no_unprefixed_test")
+        init_endpoints(app, base_path="/pfx")
+        routes = self._registered_routes(app.server)
+        for asset in self.ASSETS:
+            assert asset not in routes, (
+                f"Un-prefixed route '{asset}' should not exist when base_path is set"
+            )
+
+    def test_trailing_slash_in_base_path_is_normalised(self):
+        """Trailing slash on base_path must be stripped gracefully."""
+        from visordash import Visordash, init_endpoints
+        Visordash.endpoints_set = False
+        app = _make_dash_app("trailing_slash_test")
+        # Should not raise and should register /pfx/visordash/js.js (not //pfx/โ€ฆ)
+        init_endpoints(app, base_path="/pfx/")
+        routes = self._registered_routes(app.server)
+        assert "/pfx/visordash/js.js" in routes
+
+    def test_dashproxy_with_prefix_registers_routes(self):
+        """DashProxy + base_path must register prefixed routes on its Flask server."""
+        from visordash import init_endpoints
+        proxy = _FakeDashProxy()
+        init_endpoints(proxy, base_path="/my-solution")
+        routes = self._registered_routes(proxy.server)
+        assert "/my-solution/visordash/js.js" in routes
+        assert "/my-solution/visordash/css.css" in routes
+
+
+# ---------------------------------------------------------------------------
+# 3.  CSS font-URL rewriting
+# ---------------------------------------------------------------------------
+
+class TestCssFontRewrite:
+    """Verify the @font-face url() is rewritten at serve-time."""
+
+    @staticmethod
+    def _contains_font_url(css: str, path: str) -> bool:
+        # Accept url("...") / url('...') / url(...) with optional whitespace.
+        escaped = re.escape(path)
+        pattern = rf"url\(\s*(['\"]?){escaped}\1\s*\)"
+        return re.search(pattern, css) is not None
+
+    def _get_css(self, app: Dash, base_path: str = "") -> str:
+        """Hit the css.css endpoint and return the response body as text."""
+        url = f"{base_path}/visordash/css.css"
+        with app.server.test_client() as client:
+            response = client.get(url)
+        assert response.status_code == 200, (
+            f"GET {url} returned {response.status_code}"
+        )
+        return response.data.decode("utf-8")
+
+    def test_no_prefix_css_has_absolute_font_url(self):
+        """With no prefix, the CSS must reference /fonts/source-sans-3.woff2."""
+        from visordash import init_endpoints
+        app = _make_dash_app("css_no_prefix_test")
+        init_endpoints(app)
+        css = self._get_css(app)
+        assert self._contains_font_url(css, "/fonts/source-sans-3.woff2"), (
+            "Expected absolute font URL in CSS when no base_path is set"
+        )
+
+    def test_prefix_css_rewrites_font_url(self):
+        """With base_path=/pfx, the font URL must be /pfx/fonts/source-sans-3.woff2."""
+        from visordash import Visordash, init_endpoints
+        Visordash.endpoints_set = False
+        app = _make_dash_app("css_prefix_test")
+        init_endpoints(app, base_path="/pfx")
+        css = self._get_css(app, base_path="/pfx")
+        assert self._contains_font_url(css, "/pfx/fonts/source-sans-3.woff2"), (
+            "Font URL in CSS was not rewritten to include the base_path prefix"
+        )
+        assert not self._contains_font_url(css, "/fonts/source-sans-3.woff2"), (
+            "Old un-prefixed font URL should not appear in CSS when base_path is set"
+        )
diff --git a/tests/unit/utils/test_autosetup.py b/tests/unit/utils/test_autosetup.py
new file mode 100644
index 00000000..576628d4
--- /dev/null
+++ b/tests/unit/utils/test_autosetup.py
@@ -0,0 +1,103 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from ansys.visor.viewer.utils.autosetup import prepare_wasm_assets
+
+MODULE = "ansys.visor.viewer.utils.autosetup"
+
+
+def _setup_common_mocks(monkeypatch, version="9.3"):
+    """Set up shared mocks for metadata, regex, and trame server."""
+    monkeypatch.setattr(f"{MODULE}.metadata.version", lambda _: version)
+    monkeypatch.setattr(f"{MODULE}.re.findall", lambda pattern, text: [version])
+
+    mock_server = MagicMock()
+    monkeypatch.setattr(f"{MODULE}.trame_app.get_server", lambda: mock_server)
+
+    return mock_server
+
+
+def test_prepare_wasm_assets_linux_success(monkeypatch, tmp_path):
+    """Verify successful WASM setup on Linux platform."""
+    mock_server = _setup_common_mocks(monkeypatch)
+
+    monkeypatch.setattr(f"{MODULE}.sys.platform", "linux")
+
+    fake_site = tmp_path / "site-packages"
+    wasm_src = fake_site / "trame_vtklocal/module/serve/wasm/9.3"
+    wasm_src.mkdir(parents=True)
+
+    monkeypatch.setattr(f"{MODULE}.site.getsitepackages", lambda: [str(fake_site)])
+
+    copy_calls = {}
+
+    def fake_copytree(src, dst):
+        copy_calls["src"] = src
+        copy_calls["dst"] = dst
+
+    monkeypatch.setattr(f"{MODULE}.shutil.copytree", fake_copytree)
+    monkeypatch.setattr(f"{MODULE}.shutil.rmtree", lambda path: None)
+
+    prepare_wasm_assets(tmp_path)
+
+    assert copy_calls["src"] == wasm_src
+    assert "wasm" in str(copy_calls["dst"])
+    mock_server.enable_module.assert_called_once()
+
+
+def test_prepare_wasm_assets_windows_success(monkeypatch, tmp_path):
+    """Verify successful WASM setup on Windows platform."""
+    _setup_common_mocks(monkeypatch)
+
+    monkeypatch.setattr(f"{MODULE}.sys.platform", "win32")
+
+    wasm_src = tmp_path / ".venv/Lib/site-packages/trame_vtklocal/module/serve/wasm/9.3"
+    wasm_src.mkdir(parents=True)
+
+    monkeypatch.setattr(f"{MODULE}.shutil.copytree", lambda s, d: None)
+    monkeypatch.setattr(f"{MODULE}.shutil.rmtree", lambda p: None)
+
+    prepare_wasm_assets(tmp_path)
+
+
+def test_prepare_wasm_assets_unsupported_platform(monkeypatch, tmp_path):
+    """Ensure unsupported platforms raise RuntimeError."""
+    _setup_common_mocks(monkeypatch)
+
+    monkeypatch.setattr(f"{MODULE}.sys.platform", "darwin")
+
+    with pytest.raises(RuntimeError):
+        prepare_wasm_assets(tmp_path)
+
+
+def test_prepare_wasm_assets_missing_wasm(monkeypatch, tmp_path):
+    """Ensure missing WASM directory raises an exception."""
+    _setup_common_mocks(monkeypatch)
+
+    monkeypatch.setattr(f"{MODULE}.sys.platform", "linux")
+
+    fake_site = tmp_path / "site-packages"
+    fake_site.mkdir()
+
+    monkeypatch.setattr(f"{MODULE}.site.getsitepackages", lambda: [str(fake_site)])
+
+    with pytest.raises(Exception, match="WASM not found"):
+        prepare_wasm_assets(tmp_path)
+
+
+def test_main_calls_prepare(monkeypatch, tmp_path):
+    """Ensure main() delegates to prepare_wasm_assets with cwd."""
+    import ansys.visor.viewer.utils.autosetup as module
+
+    called = {}
+
+    def fake_prepare(path):
+        called["path"] = path
+
+    monkeypatch.setattr(module, "prepare_wasm_assets", fake_prepare)
+    monkeypatch.setattr(module.Path, "cwd", lambda: tmp_path)
+
+    module.main()
+
+    assert called["path"] == tmp_path
diff --git a/tests/unit/utils/test_perf_log_reader.py b/tests/unit/utils/test_perf_log_reader.py
new file mode 100644
index 00000000..92e98a1b
--- /dev/null
+++ b/tests/unit/utils/test_perf_log_reader.py
@@ -0,0 +1,191 @@
+import json
+from pathlib import Path
+
+import pytest
+
+from ansys.visor.viewer.utils.perf_log_reader import PerfLogReader
+
+# -----------------------------------------------------------------------------
+# Helpers
+# -----------------------------------------------------------------------------
+
+def write_log(path: Path, content: str):
+    """Overwrite the log file with the provided content."""
+    path.write_text(content, encoding="utf-8")
+
+
+def append_log(path: Path, content: str):
+    """Append additional content to the existing log file."""
+    with open(path, "a", encoding="utf-8") as f:
+        f.write(content)
+
+
+# -----------------------------------------------------------------------------
+# Log fixtures
+# -----------------------------------------------------------------------------
+
+FULL_UPDATE_VARIABLES_LOG = """[PERF] update_variables n_vars=2 | from_dict=0.01s update_variables_for_dataset=0.02s total=0.03s
+[PERF] VisorDataset.update_variables | validate=0.005s vtk_set_loop=0.006s reload=0.007s
+[PERF] update_variables_for_dataset | dataset_registry.update=0.01s update_descendant_parts=0.02s render=0.03s
+[PERF] render | RenderWindow.Render=0.02s LocalView.update=0.03s
+[PERF] _update_variable 'pressure' n_tuples=100 n_components=3 | set_loop+modified=0.02s
+[PERF] _update_variable 'temperature' n_tuples=200 n_components=1 | set_loop+modified=0.01s
+[PERF] client updateAsync | total=50 wasm=30 resize=5 states=2 (10 MB) blobs=3 (20 MB total, largest=8 MB in 12ms)
+[PERF] client onServerUpdateAsync | handler=15
+"""
+"""Representative update_variables log covering server, client, and variable parsing."""
+
+MINIMAL_UPDATE_LOG = "[PERF] update_variables n_vars=1 | total=0.05s\n"
+"""Minimal valid update_variables log with only a total phase."""
+
+ADD_DATASET_LOG = "[PERF] add_dataset | resolve_io=0.01s add_to_scene=0.02s finalize_scene=0.03s total=0.04s\n"
+"""Minimal add_dataset namespace log including total phase."""
+
+MISSING_TOTAL_LOG = "[PERF] VisorDataset.update_variables | validate=0.01s\n"
+"""Log lacking required total key, used to trigger validation failure."""
+
+NOISE_LOG = """some random log line
+[INFO] something else
+[PERF] unknown_label | foo=1
+"""
+"""Log containing no valid PERF entries for the target namespace."""
+
+
+# -----------------------------------------------------------------------------
+# Tests
+# -----------------------------------------------------------------------------
+
+def test_mark_no_log_file(tmp_path):
+    """mark() should set position to zero when log file does not exist."""
+    log_path = tmp_path / "visor.log"
+
+    reader = PerfLogReader(log_path=log_path, report_dir=tmp_path)
+    reader.mark()
+
+    assert reader._mark_pos == 0
+
+
+def test_save_report_minimal(tmp_path):
+    """save_report() should succeed with minimal valid log and produce correct fields."""
+    log_path = tmp_path / "visor.log"
+
+    write_log(log_path, "")
+    reader = PerfLogReader(log_path=log_path, report_dir=tmp_path)
+
+    reader.mark()
+    append_log(log_path, MINIMAL_UPDATE_LOG)
+
+    json_path = reader.save_report(dataset_label="test.vtp")
+    data = json.loads(json_path.read_text())
+
+    assert data["namespace"] == "update_variables"
+    assert data["dataset_label"] == "test.vtp"
+    assert data["total_s"] == pytest.approx(0.05)
+    assert data["num_variables"] == 1
+    assert data["meets_target"] is True
+
+
+def test_save_report_full_parsing(tmp_path):
+    """save_report() should correctly parse full log including phases, variables, and client data."""
+    log_path = tmp_path / "visor.log"
+
+    write_log(log_path, "")
+    reader = PerfLogReader(
+        log_path=log_path,
+        report_dir=tmp_path,
+        target_s=0.02,
+    )
+
+    reader.mark()
+    append_log(log_path, FULL_UPDATE_VARIABLES_LOG)
+
+    json_path = reader.save_report()
+    data = json.loads(json_path.read_text())
+
+    assert "from_dict_total_s" in data["phases"]
+    assert "registry_update_s" in data["phases"]
+    assert "render_scene_s" in data["phases"]
+
+    assert len(data["variables"]) == 2
+    assert data["variables"][0]["name"] == "pressure"
+    assert data["variables"][1]["name"] == "temperature"
+
+    expected_elements = 100 * 3 + 200 * 1
+    assert data["total_elements"] == expected_elements
+
+    client = data["client"]
+    assert client["total_ms"] == 50.0
+    assert client["wasm_ms"] == 30.0
+    assert client["state_total_mb"] == 10.0
+    assert client["blob_total_mb"] == 20.0
+    assert client["blob_max_mb"] == 8.0
+    assert client["blob_max_dur_ms"] == 12.0
+    assert client["handler_ms"] == 15.0
+
+    assert data["meets_target"] is False
+
+
+def test_add_dataset_namespace(tmp_path):
+    """save_report() should respect add_dataset namespace and extract total correctly."""
+    log_path = tmp_path / "visor.log"
+
+    write_log(log_path, "")
+    reader = PerfLogReader(
+        log_path=log_path,
+        report_dir=tmp_path,
+        namespace="add_dataset",
+    )
+
+    reader.mark()
+    append_log(log_path, ADD_DATASET_LOG)
+
+    json_path = reader.save_report()
+    data = json.loads(json_path.read_text())
+
+    assert data["namespace"] == "add_dataset"
+    assert data["total_s"] == pytest.approx(0.04)
+
+
+def test_save_report_missing_total_raises(tmp_path):
+    """save_report() should raise RuntimeError if required total key is absent."""
+    log_path = tmp_path / "visor.log"
+
+    write_log(log_path, "")
+    reader = PerfLogReader(log_path=log_path, report_dir=tmp_path)
+
+    reader.mark()
+    append_log(log_path, MISSING_TOTAL_LOG)
+
+    with pytest.raises(RuntimeError):
+        reader.save_report()
+
+
+def test_mark_excludes_old_lines(tmp_path):
+    """mark() should exclude log lines written before it was called."""
+    log_path = tmp_path / "visor.log"
+
+    write_log(log_path, "[PERF] update_variables n_vars=1 | total=0.1s\n")
+
+    reader = PerfLogReader(log_path=log_path, report_dir=tmp_path)
+    reader.mark()
+
+    append_log(log_path, "[PERF] update_variables n_vars=1 | total=0.05s\n")
+
+    json_path = reader.save_report()
+    data = json.loads(json_path.read_text())
+
+    assert data["total_s"] == pytest.approx(0.05)
+
+
+def test_noise_lines_ignored(tmp_path):
+    """save_report() should fail when no valid PERF lines are found after mark()."""
+    log_path = tmp_path / "visor.log"
+
+    write_log(log_path, "")
+    reader = PerfLogReader(log_path=log_path, report_dir=tmp_path)
+
+    reader.mark()
+    append_log(log_path, NOISE_LOG)
+
+    with pytest.raises(RuntimeError):
+        reader.save_report()
diff --git a/tests/unit/vtk/datasets/test_part_index.py b/tests/unit/vtk/datasets/test_part_index.py
new file mode 100644
index 00000000..391fc0e9
--- /dev/null
+++ b/tests/unit/vtk/datasets/test_part_index.py
@@ -0,0 +1,365 @@
+from dataclasses import asdict
+
+from ansys.visor.viewer.vtk.datasets.part_index import PartEntry, PartIndex
+
+
+def test_part_entry_stores_fields_correctly():
+    """PartEntry should store provided field values correctly."""
+    entry = PartEntry(part_id=10, name="blockA", flat_index=3)
+
+    assert entry.part_id == 10
+    assert entry.name == "blockA"
+    assert entry.flat_index == 3
+
+
+def test_part_entry_equality():
+    """PartEntry instances with same values should be equal."""
+    e1 = PartEntry(part_id=1, name="A", flat_index=0)
+    e2 = PartEntry(part_id=1, name="A", flat_index=0)
+
+    assert e1 == e2
+
+
+def test_part_entry_asdict_conversion():
+    """PartEntry should convert cleanly to dict via dataclasses.asdict."""
+    entry = PartEntry(part_id=5, name="my_part", flat_index=7)
+
+    result = asdict(entry)
+
+    assert result == {
+        "part_id": 5,
+        "name": "my_part",
+        "flat_index": 7,
+    }
+
+
+# ------------------------------------------------------------------
+# PartIndex tests
+# ------------------------------------------------------------------
+
+def test_non_composite_creates_single_part(monkeypatch):
+    """Non-composite datasets should produce a single part."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: 123,
+    )
+
+    data = object()
+    idx = PartIndex(data, dataset_name="ds")
+
+    parts = idx.list_parts()
+
+    assert len(parts) == 1
+    assert parts[0]["name"] == "ds"
+    assert parts[0]["part_id"] == 123
+
+
+def test_seed_id_is_used_when_available(monkeypatch):
+    """Seed IDs should override random ID generation."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: 999,
+    )
+
+    idx = PartIndex(object(), dataset_name="A", seed_ids={"A": 42})
+
+    parts = idx.list_parts()
+
+    assert parts[0]["part_id"] == 42
+
+
+def test_get_entry_returns_correct_entry(monkeypatch):
+    """get_entry should return the correct PartEntry."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: 7,
+    )
+
+    idx = PartIndex(object(), dataset_name="x")
+
+    entry = idx.get_entry(7)
+
+    assert entry is not None
+    assert entry.name == "x"
+
+
+def test_get_entry_returns_none_for_missing_id(monkeypatch):
+    """get_entry should return None for unknown part IDs."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: 1,
+    )
+
+    idx = PartIndex(object(), dataset_name="x")
+
+    assert idx.get_entry(999) is None
+
+
+def test_name_to_id_and_id_to_name_maps(monkeypatch):
+    """Maps should reflect current part assignments."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: 55,
+    )
+
+    idx = PartIndex(object(), dataset_name="abc")
+
+    assert idx.name_to_id_map == {"abc": 55}
+    assert idx.id_to_name_map == {55: "abc"}
+
+
+def test_part_ids_property(monkeypatch):
+    """part_ids should list all assigned IDs."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: 88,
+    )
+
+    idx = PartIndex(object(), dataset_name="x")
+
+    assert idx.part_ids == [88]
+
+
+def test_get_leaf_block_non_composite_returns_data(monkeypatch):
+    """Non-composite get_leaf_block should return the original data."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: 5,
+    )
+
+    data = object()
+    idx = PartIndex(data, dataset_name="x")
+
+    result = idx.get_leaf_block(5, data)
+
+    assert result is data
+
+
+def test_get_leaf_block_invalid_id_returns_none(monkeypatch):
+    """get_leaf_block should return None for invalid part ID."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: False,
+    )
+
+    idx = PartIndex(object(), dataset_name="x")
+
+    assert idx.get_leaf_block(999, object()) is None
+
+
+def test_composite_iteration_builds_multiple_parts(monkeypatch):
+    """Composite datasets should build entries for each leaf."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: True,
+    )
+
+    ids = iter([1, 2])
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: next(ids),
+    )
+
+    class FakeMeta:
+        def __init__(self, name):
+            self._name = name
+
+        def Has(self, key): # noqa: N802
+            return True
+
+        def Get(self, key): # noqa: N802
+            return self._name
+
+    class FakeIterator:
+        def __init__(self):
+            self.items = ["A", "B"]
+            self.index = 0
+
+        def SkipEmptyNodesOn(self): pass # noqa: N802
+        def IsDoneWithTraversal(self): return self.index >= len(self.items) # noqa: N802
+        def GetCurrentMetaData(self): return FakeMeta(self.items[self.index]) # noqa: N802
+        def GoToNextItem(self): self.index += 1 # noqa: N802
+        def GetCurrentDataObject(self): return self.items[self.index] # noqa: N802
+
+    class FakeComposite:
+        def NewIterator(self): # noqa: N802
+            return FakeIterator()
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.vtkCompositeDataSet",
+        type("X", (), {"NAME": staticmethod(lambda: "NAME")}),
+    )
+
+    idx = PartIndex(FakeComposite())
+    parts = idx.list_parts()
+
+    assert len(parts) == 2
+    assert {p["name"] for p in parts} == {"A", "B"}
+
+
+def test_get_leaf_block_composite_advances_iterator(monkeypatch):
+    """get_leaf_block should advance iterator based on flat_index."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: True,
+    )
+
+    ids = iter([10, 20])
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: next(ids),
+    )
+
+    class FakeMeta:
+        def __init__(self, name):
+            self._name = name
+
+        def Has(self, key): # noqa: N802
+            return True
+
+        def Get(self, key): # noqa: N802
+            return self._name
+
+    class TrackingIterator:
+        def __init__(self):
+            self.items = ["block0", "block1"]
+            self.index = 0
+            self.advance_calls = 0
+
+        def SkipEmptyNodesOn(self): pass # noqa: N802
+
+        def IsDoneWithTraversal(self): # noqa: N802
+            return self.index >= len(self.items)
+
+        def GetCurrentMetaData(self): # noqa: N802
+            return FakeMeta(self.items[self.index])
+
+        def GoToNextItem(self): # noqa: N802
+            self.index += 1
+            self.advance_calls += 1
+
+        def GetCurrentDataObject(self): # noqa: N802
+            return self.items[self.index]
+
+    class FakeComposite:
+        def NewIterator(self): # noqa: N802
+            return TrackingIterator()  # FIXED: new instance every time
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.vtkCompositeDataSet",
+        type("X", (), {"NAME": staticmethod(lambda: "NAME")}),
+    )
+
+    data = FakeComposite()
+    idx = PartIndex(data)
+
+    # preserve insertion order instead of sorting
+    second_part_id = idx.part_ids[1]
+
+    result = idx.get_leaf_block(second_part_id, data)
+
+    assert result == "block1"
+
+def test_duplicate_names_emit_warning(monkeypatch):
+    """Duplicate part names should trigger a logger warning."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.is_composite_dataset",
+        lambda data: True,
+    )
+
+    # deterministic IDs (values don't matter here)
+    ids = iter([1, 2])
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.get_random_javascript_safe_id",
+        lambda: next(ids),
+    )
+
+    class FakeMeta:
+        def __init__(self, name):
+            self._name = name
+
+        def Has(self, key): # noqa: N802
+            return True
+
+        def Get(self, key): # noqa: N802
+            return self._name
+
+    class FakeIterator:
+        def __init__(self):
+            # duplicate names
+            self.items = ["dup", "dup"]
+            self.index = 0
+
+        def SkipEmptyNodesOn(self): pass # noqa: N802
+
+        def IsDoneWithTraversal(self): # noqa: N802
+            return self.index >= len(self.items)
+
+        def GetCurrentMetaData(self): # noqa: N802
+            return FakeMeta(self.items[self.index])
+
+        def GoToNextItem(self): # noqa: N802
+            self.index += 1
+
+    class FakeComposite:
+        def NewIterator(self): # noqa: N802
+            return FakeIterator()
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.vtkCompositeDataSet",
+        type("X", (), {"NAME": staticmethod(lambda: "NAME")}),
+    )
+
+    warnings = []
+
+    def fake_warning(msg):
+        warnings.append(msg)
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.part_index.logger.warning",
+        fake_warning,
+    )
+
+    PartIndex(FakeComposite())
+
+    assert len(warnings) == 1
+    assert "Duplicate part name 'dup'" in warnings[0]
diff --git a/tests/unit/vtk/datasets/test_visor_dataset.py b/tests/unit/vtk/datasets/test_visor_dataset.py
new file mode 100644
index 00000000..5b89d6ef
--- /dev/null
+++ b/tests/unit/vtk/datasets/test_visor_dataset.py
@@ -0,0 +1,616 @@
+from unittest.mock import MagicMock, patch
+
+import numpy as np
+import pytest
+from vtkmodules.util.vtkConstants import VTK_FLOAT
+
+from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType
+
+# Target import
+from ansys.visor.viewer.vtk.datasets.visor_dataset import VisorDataset
+
+
+# Lightweight mocks for external enum and types
+class MockVisorVtkVariableType:
+    POINT = object()
+    CELL = object()
+
+class MockVisorDatasetInfo:
+    def __init__(self, name="dataset_name", id=123, unit="mm", file_path=None, metadata_path=None):
+        self.name = name
+        self.id = id
+        self.unit = unit
+        self.file_path = file_path
+        self.metadata_path = metadata_path
+
+    def model_dump(self):
+        return {"name": self.name, "id": self.id, "unit": self.unit, "file_path": self.file_path, "metadata_path": self.metadata_path}
+
+class MockVisorDatasetState:
+    def __init__(self, state_dict=None):
+        self._state_dict = state_dict or {"foo": "bar", "parts": ["p1", "p2"]}
+
+    def model_dump(self, exclude=None):
+        return dict(self._state_dict)
+
+
+@pytest.fixture
+def mock_data_object():
+    """PyTest fixture for mock data object."""
+    # Mock of vtkDataObject with Modified
+    m = MagicMock()
+    m.Modified = MagicMock()
+    return m
+
+
+
+
+
+@pytest.fixture
+def mock_info():
+    """PyTest fixture for mock info."""
+    return MockVisorDatasetInfo()
+
+
+@pytest.fixture
+def mock_state():
+    """PyTest fixture for mock state."""
+    return MockVisorDatasetState()
+
+
+@pytest.fixture
+def dataset_factory(mock_data_object):
+    """PyTest fixture for dataset factory."""
+    def make_dataset(is_composite=False, variables_instance=None):
+        # Patch `is_composite_dataset` to control composite status
+        is_comp_patch = patch(
+            "ansys.visor.viewer.vtk.datasets.visor_dataset.is_composite_dataset",
+            return_value=is_composite,
+        )
+        is_comp_ctx = is_comp_patch.start()
+
+        # Patch `VisorVariables` to return provided or mocked instance
+        if variables_instance is None and not is_composite:
+            variables_instance = MagicMock()
+        tv_patch = patch(
+            "ansys.visor.viewer.vtk.datasets.visor_dataset.VisorVariables",
+            return_value=variables_instance,
+        )
+        tv_ctx = tv_patch.start()
+
+        # Patch PartIndex so it doesn't try to traverse the mock VTK object
+        mock_part_index = MagicMock()
+        mock_part_index.name_to_id_map = {}
+        mock_part_index.part_ids = [] if is_composite else [999]
+        mock_part_index.get_entry.return_value = None
+        mock_part_index.get_leaf_block.return_value = mock_data_object
+        pi_patch = patch(
+            "ansys.visor.viewer.vtk.datasets.visor_dataset.PartIndex",
+            return_value=mock_part_index,
+        )
+        pi_ctx = pi_patch.start()
+
+        # Create mock metadata with required structure
+        mock_metadata = MagicMock()
+        mock_metadata.unit = "mm"
+        mock_metadata.file_path = None
+        mock_metadata.metadata_path = None
+        mock_metadata.state.parts = {}  # Empty parts dict for simplicity
+
+        # Use correct constructor signature
+        ds = VisorDataset(
+            id=123,
+            name="dataset_name",
+            data=mock_data_object,
+            part_name_to_id={},
+            metadata=mock_metadata
+        )
+
+        # Cleanup function to stop patches
+        def _cleanup():
+            is_comp_ctx.stop()
+            tv_ctx.stop()
+            pi_ctx.stop()
+
+        return ds, variables_instance, _cleanup
+
+    return make_dataset
+
+
+def test_name_returns_state_name(dataset_factory):
+    """Test that name returns state name."""
+    ds, _, cleanup = dataset_factory(is_composite=False)
+    try:
+        assert ds.name == "dataset_name"
+    finally:
+        cleanup()
+
+
+def test_state_info_excludes_parts(dataset_factory, mock_state):
+    """Test that state info excludes parts."""
+    ds, _, cleanup = dataset_factory(is_composite=False)
+    try:
+        info = ds.info_dict
+        assert "parts" not in info
+        assert info == {"name": "dataset_name", "id": 123, "unit": "mm", "file_path": None, "metadata_path": None}
+    finally:
+        cleanup()
+
+
+def test_is_composite_true(dataset_factory):
+    """Test that is composite is true."""
+    ds, _, cleanup = dataset_factory(is_composite=True)
+    try:
+        assert ds.is_composite() is True
+    finally:
+        cleanup()
+
+
+def test_is_composite_false(dataset_factory):
+    """Test that is composite is false."""
+    ds, _, cleanup = dataset_factory(is_composite=False)
+    try:
+        assert ds.is_composite() is False
+    finally:
+        cleanup()
+
+
+def test_list_variables_on_composite_returns_per_part(dataset_factory):
+    """Composite datasets now return a per-part list (not raise NotImplementedError)."""
+    ds, _, cleanup = dataset_factory(is_composite=True, variables_instance=None)
+    try:
+        # With the mock PartIndex returning part_ids=[], the result should be an empty list.
+        result = ds.list_variables()
+        assert isinstance(result, list)
+    finally:
+        cleanup()
+
+
+def test_list_variables_calls_variables_list(dataset_factory):
+    """Test that list variables calls variables list."""
+    ds, variables, cleanup = dataset_factory(is_composite=False)
+    variables.list.return_value = ["var1", "var2"]
+    try:
+        result = ds.list_variables()
+        variables.list.assert_called_once()
+        # list_variables now returns List[VisorPartVariables]; check the variables on the first entry
+        assert len(result) == 1
+        assert result[0].variables == ["var1", "var2"]
+    finally:
+        cleanup()
+
+
+def test_update_variables_on_composite_broadcast_applies_to_all_matching_parts(dataset_factory):
+    """part_id=None on a composite dataset broadcasts to every part where validation passes."""
+    # Build a dataset_factory-style composite with two parts
+    mock_vars_0 = MagicMock()
+    mock_vars_0.validate_new_variables = MagicMock(return_value=True)
+    mock_vars_1 = MagicMock()
+    mock_vars_1.validate_new_variables = MagicMock(return_value=True)
+
+    is_comp_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.is_composite_dataset",
+        return_value=True,
+    )
+    is_comp_ctx = is_comp_patch.start()
+
+    part_id_0, part_id_1 = 111, 222
+    mock_part_index = MagicMock()
+    mock_part_index.name_to_id_map = {}
+    mock_part_index.id_to_name_map = {}
+    mock_part_index.part_ids = [part_id_0, part_id_1]
+    mock_part_index.get_entry.return_value = None
+    mock_part_index.get_leaf_block.return_value = MagicMock()
+    pi_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.PartIndex",
+        return_value=mock_part_index,
+    )
+    pi_ctx = pi_patch.start()
+
+    call_count = [0]
+    def fake_variables_factory(*args, **kwargs):
+        call_count[0] += 1
+        return mock_vars_0 if call_count[0] == 1 else mock_vars_1
+    tv_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.VisorVariables",
+        side_effect=fake_variables_factory,
+    )
+    tv_ctx = tv_patch.start()
+
+    mock_metadata = MagicMock()
+    mock_metadata.unit = "mm"
+    mock_metadata.file_path = None
+    mock_metadata.metadata_path = None
+    mock_metadata.state.parts = {}
+    mock_data = MagicMock()
+
+    ds = VisorDataset(id=1, name="ds", data=mock_data, part_name_to_id={}, metadata=mock_metadata)
+
+    broadcast_var = MagicMock()
+    broadcast_var.part_id = None  # broadcast
+
+    try:
+        with patch.object(ds, "_update_variable") as upd:
+            ds.update_variables([broadcast_var])
+            # Should have been called once per part
+            assert upd.call_count == 2
+        mock_vars_0.reload.assert_called_once()
+        mock_vars_1.reload.assert_called_once()
+    finally:
+        is_comp_ctx.stop()
+        tv_ctx.stop()
+        pi_ctx.stop()
+
+
+def test_update_variables_on_composite_broadcast_skips_non_matching_parts(dataset_factory):
+    """Broadcast skips parts where validation fails (e.g. variable doesn't exist)."""
+    mock_vars_ok = MagicMock()
+    mock_vars_ok.validate_new_variables = MagicMock(return_value=True)
+    mock_vars_bad = MagicMock()
+    mock_vars_bad.validate_new_variables = MagicMock(return_value=False)
+
+    is_comp_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.is_composite_dataset",
+        return_value=True,
+    )
+    is_comp_ctx = is_comp_patch.start()
+
+    part_id_ok, part_id_bad = 111, 222
+    mock_part_index = MagicMock()
+    mock_part_index.name_to_id_map = {}
+    mock_part_index.id_to_name_map = {}
+    mock_part_index.part_ids = [part_id_ok, part_id_bad]
+    mock_part_index.get_entry.return_value = None
+    mock_part_index.get_leaf_block.return_value = MagicMock()
+    pi_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.PartIndex",
+        return_value=mock_part_index,
+    )
+    pi_ctx = pi_patch.start()
+
+    call_count = [0]
+    def fake_variables_factory(*args, **kwargs):
+        call_count[0] += 1
+        return mock_vars_ok if call_count[0] == 1 else mock_vars_bad
+    tv_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.VisorVariables",
+        side_effect=fake_variables_factory,
+    )
+    tv_ctx = tv_patch.start()
+
+    mock_metadata = MagicMock()
+    mock_metadata.unit = "mm"
+    mock_metadata.file_path = None
+    mock_metadata.metadata_path = None
+    mock_metadata.state.parts = {}
+
+    ds = VisorDataset(id=1, name="ds", data=MagicMock(), part_name_to_id={}, metadata=mock_metadata)
+
+    broadcast_var = MagicMock()
+    broadcast_var.part_id = None
+
+    try:
+        with patch.object(ds, "_update_variable") as upd:
+            ds.update_variables([broadcast_var])
+            # Only the matching part should be updated
+            assert upd.call_count == 1
+        mock_vars_ok.reload.assert_called_once()
+        mock_vars_bad.reload.assert_not_called()
+    finally:
+        is_comp_ctx.stop()
+        tv_ctx.stop()
+        pi_ctx.stop()
+
+
+def test_update_variables_validation_failure_raises(dataset_factory):
+    """Test that updating variables validation failure raises."""
+    ds, variables, cleanup = dataset_factory(is_composite=False)
+    variables.validate_new_variables.return_value = False
+    try:
+        with pytest.raises(ValueError):
+            ds.update_variables(variable_list=[MagicMock()])
+        variables.validate_new_variables.assert_called_once()
+        variables.reload.assert_not_called()
+    finally:
+        cleanup()
+
+
+def test_update_variables_happy_path_calls_reload_and_modifies(dataset_factory, mock_data_object):
+    """Test that updating variables happy path calls reload and modifies."""
+    ds, variables, cleanup = dataset_factory(is_composite=False)
+    variables.validate_new_variables.return_value = True
+
+    # Create a mock update object; content is irrelevant due to heavy mocking
+    mock_update = MagicMock()
+
+    # Patch the internal `_update_variable` to avoid touching private behavior but ensure it gets invoked via public API
+    with patch.object(ds, "_update_variable") as upd:
+        ds.update_variables(variable_list=[mock_update, mock_update])
+        variables.validate_new_variables.assert_called_once_with([mock_update, mock_update])
+        assert upd.call_count == 2
+        variables.reload.assert_called_once_with(mock_data_object)
+        # Note: The implementation no longer calls data.Modified() after reload.
+        mock_data_object.Modified.assert_not_called()
+
+
+def test_update_variables_empty_list_validates_and_reloads(dataset_factory, mock_data_object):
+    """Test that updating variables empty list validates and reloads."""
+    ds, variables, cleanup = dataset_factory(is_composite=False)
+    variables.validate_new_variables.return_value = True
+    with patch.object(ds, "_update_variable") as upd:
+        ds.update_variables(variable_list=[])
+        variables.validate_new_variables.assert_called_once_with([])
+        upd.assert_not_called()
+        variables.reload.assert_called_once_with(mock_data_object)
+
+
+@pytest.fixture
+def mock_point_data():
+    """PyTest fixture for mock point data."""
+    return MagicMock()
+
+@pytest.fixture
+def mock_cell_data():
+    """PyTest fixture for mock cell data."""
+    return MagicMock()
+
+@pytest.fixture
+def mock_array():
+    """PyTest fixture for mock array."""
+    a = MagicMock()
+    a.GetNumberOfTuples.return_value = 3
+    a.GetDataType.return_value = VTK_FLOAT
+    a.GetName.return_value = "pressure"
+    return a
+
+@pytest.fixture
+def mock_data_object(mock_point_data, mock_cell_data):  # noqa: F811
+    """PyTest fixture for mock data object."""
+    m = MagicMock()
+    # Expose VTK-like API used by _get_data_for_type
+    m.GetPointData.return_value = mock_point_data
+    m.GetCellData.return_value = mock_cell_data
+    m.Modified = MagicMock()
+    return m
+
+@pytest.fixture
+def mock_state_class():
+    """PyTest fixture for mock state class."""
+    class MockState:
+        name = "dataset_name"
+        def model_dump(self, exclude=None):
+            return {"foo": "bar"} if exclude == "parts" else {"foo": "bar", "parts": ["p1"]}
+    return MockState()
+
+
+@pytest.fixture
+def dataset_with_variables(mock_data_object):
+    """PyTest fixture for dataset with variables."""
+    # Ensure non-composite
+    is_comp_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.is_composite_dataset",
+        return_value=False,
+    )
+    is_comp_ctx = is_comp_patch.start()
+
+    # Provide a VisorVariables mock
+    variables = MagicMock()
+    tv_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.VisorVariables",
+        return_value=variables,
+    )
+    tv_ctx = tv_patch.start()
+
+    # Patch PartIndex so it doesn't traverse the mock VTK object
+    mock_part_index = MagicMock()
+    mock_part_index.name_to_id_map = {}
+    mock_part_index.part_ids = [999]
+    mock_part_index.get_entry.return_value = None
+    mock_part_index.get_leaf_block.return_value = mock_data_object
+    pi_patch = patch(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset.PartIndex",
+        return_value=mock_part_index,
+    )
+    pi_ctx = pi_patch.start()
+
+    # Create mock metadata
+    mock_metadata = MagicMock()
+    mock_metadata.unit = "mm"
+    mock_metadata.file_path = None
+    mock_metadata.metadata_path = None
+    mock_metadata.state.parts = {}
+
+    ds = VisorDataset(
+        id=1,
+        name="dataset_name",
+        data=mock_data_object,
+        part_name_to_id={},
+        metadata=mock_metadata
+    )
+
+    def _cleanup():
+        is_comp_ctx.stop()
+        tv_ctx.stop()
+        pi_ctx.stop()
+
+    return ds, variables, _cleanup
+
+
+def test_update_variables_point_scalar_updates(dataset_with_variables, mock_point_data, mock_array):
+    """Test that updating variables point scalar updates."""
+    ds, variables, cleanup = dataset_with_variables
+    variables.validate_new_variables.return_value = True
+
+    # Prepare point data to return our array
+    mock_point_data.GetArray.return_value = mock_array
+
+    # Variable update: scalar values on POINT data
+    update = MagicMock()
+    update.type = VisorVtkVariableType.POINT
+    update.name = "pressure"
+    update.num_components = 1
+    update.data = np.array([[1.0], [2.0], [3.0]], dtype=float)
+
+    try:
+        # Patch vtk_to_numpy to return a real numpy buffer view so np.copyto works
+        with patch("ansys.visor.viewer.vtk.datasets.visor_dataset.vtk_to_numpy") as vtknp:
+            vtknp.return_value = np.zeros((mock_array.GetNumberOfTuples.return_value, update.num_components), dtype=float)
+            ds.update_variables([update])
+
+        variables.validate_new_variables.assert_called_once_with([update])
+        # Implementation writes into the existing array buffer and calls Modified() on the array
+        mock_array.Modified.assert_called_once()
+        # The dataset object itself is not Modified by the current implementation
+        ds.data.Modified.assert_not_called()
+        variables.reload.assert_called_once_with(ds.data)
+    finally:
+        cleanup()
+
+
+def test_update_variables_cell_vector_updates(dataset_with_variables, mock_cell_data, mock_array):
+    """Test that updating variables cell vector updates."""
+    ds, variables, cleanup = dataset_with_variables
+    variables.validate_new_variables.return_value = True
+
+    mock_cell_data.GetArray.return_value = mock_array
+
+    update = MagicMock()
+    update.type = VisorVtkVariableType.CELL
+    update.name = "velocity"
+    update.num_components = 3
+    update.data = np.array([[1.0, 0.0, 0.0],
+                            [0.0, 1.0, 0.0],
+                            [0.0, 0.0, 1.0]], dtype=float)
+
+    try:
+        # Patch vtk_to_numpy so np.copyto can operate on a concrete numpy buffer
+        with patch("ansys.visor.viewer.vtk.datasets.visor_dataset.vtk_to_numpy") as vtknp:
+            vtknp.return_value = np.zeros((mock_array.GetNumberOfTuples.return_value, update.num_components), dtype=float)
+            ds.update_variables([update])
+
+        variables.validate_new_variables.assert_called_once_with([update])
+        mock_array.Modified.assert_called_once()
+        ds.data.Modified.assert_not_called()
+        variables.reload.assert_called_once_with(ds.data)
+    finally:
+        cleanup()
+
+
+def test_update_variables_array_not_found_raises(dataset_with_variables, mock_point_data):
+    """Test that updating variables array not found raises."""
+    ds, variables, cleanup = dataset_with_variables
+    variables.validate_new_variables.return_value = True
+
+    mock_point_data.GetArray.return_value = None
+
+    update = MagicMock()
+    update.type = VisorVtkVariableType.POINT
+    update.name = "missing"
+    update.num_components = 1
+    update.data = np.array([[0.0], [0.0], [0.0]], dtype=float)
+
+    try:
+        with pytest.raises(ValueError, match="Variable 'missing' not found"):
+            ds.update_variables([update])
+        variables.reload.assert_not_called()
+        ds.data.Modified.assert_not_called()
+    finally:
+        cleanup()
+
+
+def test_update_variables_length_mismatch_raises(dataset_with_variables, mock_point_data, mock_array):
+    """Test that updating variables length mismatch raises."""
+    ds, variables, cleanup = dataset_with_variables
+    variables.validate_new_variables.return_value = True
+
+    # Force array length that will mismatch with provided data
+    mock_array.GetNumberOfTuples.return_value = 2
+    mock_point_data.GetArray.return_value = mock_array
+
+    update = MagicMock()
+    update.type = VisorVtkVariableType.POINT
+    update.name = "pressure"
+    update.num_components = 1
+    update.data = np.array([[1.0], [2.0], [3.0]], dtype=float)  # 3 items vs 2 tuples
+
+    try:
+        with pytest.raises(ValueError, match="Data length mismatch"):
+            ds.update_variables([update])
+        variables.reload.assert_not_called()
+        ds.data.Modified.assert_not_called()
+        mock_array.Modified.assert_not_called()
+        mock_array.DeepCopy.assert_not_called()
+    finally:
+        cleanup()
+
+
+def test_update_variables_unsupported_type_raises(dataset_with_variables):
+    """Test that updating variables unsupported type raises."""
+    ds, variables, cleanup = dataset_with_variables
+    variables.validate_new_variables.return_value = True
+
+    # Use an unsupported type that is not POINT or CELL
+    unsupported_type = object()
+
+    update = MagicMock()
+    update.type = unsupported_type
+    update.name = "var"
+    update.num_components = 1
+    update.data = np.array([[0.0], [0.0], [0.0]], dtype=float)
+
+    try:
+        with pytest.raises(ValueError, match="Unsupported variable type"):
+            ds.update_variables([update])
+        variables.reload.assert_not_called()
+        ds.data.Modified.assert_not_called()
+    finally:
+        cleanup()
+
+
+# ================================================================== #
+# is_dirty / mark_clean
+# ================================================================== #
+
+def test_new_dataset_is_dirty(dataset_factory):
+    """Test that new dataset is is dirty."""
+    ds, _, cleanup = dataset_factory(is_composite=False)
+    try:
+        assert ds.is_dirty is True
+    finally:
+        cleanup()
+
+
+def test_mark_clean_clears_dirty_flag(dataset_factory):
+    """Test that mark clean clears dirty flag."""
+    ds, _, cleanup = dataset_factory(is_composite=False)
+    try:
+        ds.mark_clean()
+        assert ds.is_dirty is False
+    finally:
+        cleanup()
+
+
+def test_update_variables_marks_dataset_dirty(dataset_with_variables, mock_point_data, mock_array):
+    """Test that updating variables marks dataset is dirty."""
+    ds, variables, cleanup = dataset_with_variables
+    variables.validate_new_variables.return_value = True
+    mock_point_data.GetArray.return_value = mock_array
+
+    update = MagicMock()
+    update.type = VisorVtkVariableType.POINT
+    update.name = "pressure"
+    update.num_components = 1
+    update.data = np.array([[1.0], [2.0], [3.0]], dtype=float)
+
+    try:
+        ds.mark_clean()
+        assert ds.is_dirty is False
+        # Patch vtk_to_numpy to allow the update to succeed without TypeError
+        with patch("ansys.visor.viewer.vtk.datasets.visor_dataset.vtk_to_numpy") as vtknp:
+            vtknp.return_value = np.zeros((mock_array.GetNumberOfTuples.return_value, update.num_components), dtype=float)
+            ds.update_variables([update])
+
+        assert ds.is_dirty is True
+    finally:
+        cleanup()
+
diff --git a/tests/unit/vtk/datasets/test_visor_dataset_registry.py b/tests/unit/vtk/datasets/test_visor_dataset_registry.py
new file mode 100644
index 00000000..d55ee2d6
--- /dev/null
+++ b/tests/unit/vtk/datasets/test_visor_dataset_registry.py
@@ -0,0 +1,182 @@
+# python
+from unittest.mock import MagicMock
+
+import pytest
+
+from ansys.visor.viewer.vtk.datasets.visor_dataset_registry import VisorDatasetRegistry
+
+
+@pytest.fixture
+def registry():
+    """Provide an empty VisorDatasetRegistry instance."""
+    return VisorDatasetRegistry()
+
+
+def make_mock_dataset(name="ds", info_dict=None, parts=None, variables=None):
+    """Helper function to create a mock dataset with specified attributes."""
+    mock = MagicMock()
+    mock.name = name
+    mock.info_dict = info_dict if info_dict is not None else {"info": name}
+    mock.state = MagicMock()
+    mock.state.parts = parts if parts is not None else {}
+    mock.list_variables.return_value = variables if variables is not None else []
+    return mock
+
+
+def test_count_returns_number_of_datasets(registry):
+    """Verify that count returns the number of registered datasets."""
+    assert registry.count == 0
+    registry.datasets = {1: make_mock_dataset(), 2: make_mock_dataset()}
+    assert registry.count == 2
+
+
+def test_list_state_info_maps_ids_to_state_info(registry):
+    """Verify that dataset IDs are mapped to their state information."""
+    ds1 = make_mock_dataset(info_dict={"a": 1})
+    ds2 = make_mock_dataset(info_dict={"b": 2})
+    registry.datasets = {10: ds1, 20: ds2}
+
+    info = registry.list_info()
+    assert info == {10: {"a": 1}, 20: {"b": 2}}
+
+
+def test_add_registers_dataset_and_returns_id(monkeypatch, registry):
+    """Verify that adding a dataset registers it and returns its ID."""
+    # Patch VisorDataset where it is referenced by the registry
+    created = {}
+
+    class FakeDataset:
+        def __init__(self, dataset_id, dataset_name, input, part_name_to_id, metadata):
+            created["args"] = (dataset_id, dataset_name, input, part_name_to_id, metadata)
+            self.name = dataset_name
+            self.state = MagicMock()
+            self.state.parts = {}
+            self.info_dict = {"id": dataset_id}
+            self.list_variables = MagicMock(return_value=[])
+            self.update_variables = MagicMock()
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.datasets.visor_dataset_registry.VisorDataset",
+        FakeDataset,
+    )
+
+    mock_input = MagicMock()
+    mock_metadata = MagicMock()
+    mock_metadata.unit = "mm"
+
+    ret = registry.add(5, "test_dataset", mock_input, {}, mock_metadata)
+
+    assert isinstance(ret, FakeDataset)
+    assert 5 in registry.datasets
+    assert created["args"] == (5, "test_dataset", mock_input, {}, mock_metadata)
+    assert registry.unit == "mm"  # verify _update_unit was called
+
+
+def test_remove_deletes_dataset_if_present(registry):
+    """Verify that removing a dataset deletes it from the registry."""
+    ds = make_mock_dataset()
+    registry.datasets = {7: ds}
+    registry.remove(7)
+    assert registry.datasets == {}
+
+    registry.remove(99)  # should be a no-op
+    assert registry.datasets == {}
+
+
+def test_get_sanitized_metadata_name_unique(registry):
+    """Verify that unique dataset names are returned unchanged."""
+    metadata = MagicMock()
+    metadata.name = "unique"
+    new_name = registry.get_sanitized_metadata_name(metadata)
+    assert new_name == "unique"
+
+
+def test_get_sanitized_metadata_name_duplicate_warns_and_renames(registry):
+    """Verify that duplicate dataset names are renamed to remain unique."""
+    ds_existing = make_mock_dataset(name="data")
+    registry.datasets = {1: ds_existing}
+
+    metadata = MagicMock()
+    metadata.name = "data"
+
+    new_name = registry.get_sanitized_metadata_name(metadata)
+
+    assert new_name.startswith("data_")
+    assert new_name != "data"
+
+
+def test_get_sanitized_metadata_name_none_raises(registry):
+    """Verify that a missing metadata object raises a ValueError."""
+    with pytest.raises(ValueError):
+        registry.get_sanitized_metadata_name(None)
+
+
+def test_clear_resets_datasets_and_unit(registry):
+    """Verify that clear removes all datasets and resets the unit."""
+    registry.datasets = {1: make_mock_dataset()}
+    registry.unit = "m"
+    registry.clear()
+    assert registry.datasets == {}
+    assert registry.unit == ""
+
+
+def test_list_variables_returns_from_dataset(registry):
+    """Verify that variable names are returned from the requested dataset."""
+    ds = make_mock_dataset(variables=["v1", "v2"])
+    registry.datasets = {42: ds}
+    vars_ = registry.list_variables(42)
+    assert vars_ == ["v1", "v2"]
+    ds.list_variables.assert_called_once()
+
+
+def test_list_variables_missing_raises(registry):
+    """Verify that requesting variables for an unknown dataset raises an error."""
+    with pytest.raises(ValueError):
+        registry.list_variables(999)
+
+
+def test_update_variables_calls_dataset_method(registry):
+    """Verify that variable updates are forwarded to the dataset."""
+    ds = make_mock_dataset()
+    registry.datasets = {11: ds}
+    updates = [MagicMock(), MagicMock()]
+    registry.update_variables(11, updates)
+    ds.update_variables.assert_called_once_with(updates)
+
+
+def test_update_variables_missing_raises(registry):
+    """Verify that updating variables on an unknown dataset raises an error."""
+    with pytest.raises(ValueError):
+        registry.update_variables(123, [MagicMock()])
+
+
+def test_update_unit_sets_on_first_add(registry):
+    """Verify that the registry unit is initialized from the first dataset."""
+    metadata = MagicMock()
+    metadata.unit = "mm"
+    registry._update_unit(metadata)
+    assert registry.unit == "mm"
+
+
+def test_update_unit_mismatch_warns_and_resets(registry):
+    """Verify that a unit mismatch triggers the registry's mismatch handling."""
+    registry.datasets = {1: make_mock_dataset()}
+    registry.unit = "m"
+
+    metadata = MagicMock()
+    metadata.unit = "s"
+
+    registry._update_unit(metadata)
+
+    assert registry.unit == ""
+
+
+def test_update_unit_matches_keeps_unit(registry):
+    """Verify that matching dataset units leave the registry unit unchanged."""
+    registry.datasets = {1: make_mock_dataset()}
+    registry.unit = "m"
+    metadata = MagicMock()
+    metadata.unit = "m"
+
+    registry._update_unit(metadata)
+    assert registry.unit == "m"
diff --git a/tests/unit/vtk/io/test_dataset_to_file.py b/tests/unit/vtk/io/test_dataset_to_file.py
new file mode 100644
index 00000000..bfa76287
--- /dev/null
+++ b/tests/unit/vtk/io/test_dataset_to_file.py
@@ -0,0 +1,43 @@
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+from ansys.visor.viewer.vtk.io.dataset_to_file import dataset_to_file
+
+
+class TestDatasetToFile:
+    def test_creates_parent_directory_if_absent(self, tmp_path):
+        """Verify that dataset_to_file creates parent directory if is absent."""
+        dest = tmp_path / "subdir" / "snap.vtkhdf"
+        mock_writer = MagicMock()
+        with patch("ansys.visor.viewer.vtk.io.dataset_to_file.vtkHDFWriter", return_value=mock_writer):
+            dataset_to_file(dest, MagicMock())
+        assert dest.parent.exists()
+
+    def test_calls_writer_with_correct_filename_and_input(self, tmp_path):
+        """Verify that dataset_to_file calls writer with correct filename and input."""
+        dest = tmp_path / "snap.vtkhdf"
+        dataset = MagicMock()
+        mock_writer = MagicMock()
+        with patch("ansys.visor.viewer.vtk.io.dataset_to_file.vtkHDFWriter", return_value=mock_writer):
+            dataset_to_file(dest, dataset)
+        mock_writer.SetFileName.assert_called_once_with(str(dest))
+        mock_writer.SetInputData.assert_called_once_with(dataset)
+        mock_writer.Write.assert_called_once()
+
+    def test_returns_path_object(self, tmp_path):
+        """Verify that dataset_to_file returns path object."""
+        dest = tmp_path / "snap.vtkhdf"
+        mock_writer = MagicMock()
+        with patch("ansys.visor.viewer.vtk.io.dataset_to_file.vtkHDFWriter", return_value=mock_writer):
+            result = dataset_to_file(dest, MagicMock())
+        assert isinstance(result, Path)
+        assert result == dest
+
+    def test_accepts_string_path(self, tmp_path):
+        """Verify that dataset_to_file accepts string path."""
+        dest = tmp_path / "snap.vtkhdf"
+        mock_writer = MagicMock()
+        with patch("ansys.visor.viewer.vtk.io.dataset_to_file.vtkHDFWriter", return_value=mock_writer):
+            result = dataset_to_file(str(dest), MagicMock())
+        assert result == dest
+
diff --git a/tests/unit/vtk/io/test_file_to_dataset.py b/tests/unit/vtk/io/test_file_to_dataset.py
new file mode 100644
index 00000000..0ce7f588
--- /dev/null
+++ b/tests/unit/vtk/io/test_file_to_dataset.py
@@ -0,0 +1,142 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+from vtkmodules.vtkCommonDataModel import vtkPolyData
+
+from ansys.visor.viewer.vtk.io.file_to_dataset import file_to_dataset
+
+
+@pytest.mark.parametrize("ext,reader_cls", [
+    ("vtu", "vtkXMLUnstructuredGridReader"),
+    ("vtp", "vtkXMLPolyDataReader"),
+    ("vtm", "vtkXMLMultiBlockDataReader"),
+    ("vtkhdf", "vtkHDFReader"),
+])
+def test_file_to_dataset_supported_extensions(ext, reader_cls):
+    """Verify that file_to_dataset dispatches to the correct reader class for supported extensions."""
+    file_path = f"/tmp/test.{ext}"
+    # Use a real vtkPolyData so DeepCopy works and the vtkhdf isinstance check passes.
+    real_output = vtkPolyData()
+    real_output.GetPoints()  # initialise internal structure
+    with patch("os.path.exists", return_value=True), \
+         patch("os.path.basename", return_value=f"test.{ext}"), \
+         patch("os.path.splitext", return_value=("test", f".{ext}")), \
+         patch(f"ansys.visor.viewer.vtk.io.file_to_dataset.{reader_cls}") as mock_reader_cls:
+        mock_reader = MagicMock()
+        mock_reader.GetOutput.return_value = real_output
+        mock_reader_cls.return_value = mock_reader
+        result = file_to_dataset(file_path)
+        mock_reader.SetFileName.assert_any_call(file_path)
+        mock_reader.Update.assert_called_once()
+        # We return a deep copy, not the original object โ€” check type instead of identity.
+        assert isinstance(result, vtkPolyData)
+
+
+def test_file_to_dataset_file_not_exists():
+    """Verify that file_to_dataset raises RuntimeError when the file does not exist."""
+    file_path = "/tmp/missing.vtu"
+    with patch("os.path.exists", return_value=False):
+        with pytest.raises(RuntimeError, match="file does not exist"):
+            file_to_dataset(file_path)
+
+
+def test_file_to_dataset_unsupported_extension():
+    """Verify that file_to_dataset raises RuntimeError for unsupported file extensions."""
+    file_path = "/tmp/test.txt"
+    with patch("os.path.exists", return_value=True), \
+         patch("os.path.basename", return_value="test.txt"), \
+         patch("os.path.splitext", return_value=("test", ".txt")):
+        with pytest.raises(RuntimeError, match="Unsupported file"):
+            file_to_dataset(file_path)
+
+
+def test_file_to_dataset_reader_raises_runtimeerror():
+    """Verify that file_to_dataset raises RuntimeError when the reader fails."""
+    file_path = "/tmp/test.vtu"
+    with patch("os.path.exists", return_value=True), \
+         patch("os.path.basename", return_value="test.vtu"), \
+         patch("os.path.splitext", return_value=("test", ".vtu")), \
+         patch("ansys.visor.viewer.vtk.io.file_to_dataset.vtkXMLUnstructuredGridReader") as mock_reader_cls:
+        mock_reader = MagicMock()
+        mock_reader.Update.side_effect = RuntimeError("reader error")
+        mock_reader_cls.return_value = mock_reader
+        with pytest.raises(RuntimeError, match="could not create dataset from file: reader error"):
+            file_to_dataset(file_path)
+
+
+# ------------------------------------------------------------------ #
+# vtkhdf-specific unit tests
+# ------------------------------------------------------------------ #
+
+@pytest.mark.parametrize("original_filename", [
+    "model.VTKHDF",
+    "model.VtkHdf",
+    "model.VTKHDF",
+])
+def test_file_to_dataset_vtkhdf_extension_is_case_insensitive(original_filename):
+    """Extension matching must be case-insensitive for .vtkhdf files.
+
+    The real code does:
+        filename = os.path.basename(file_path).lower()
+        extension = os.path.splitext(filename)[1][1:]
+    so os.path.basename must return the already-lowered name, and
+    os.path.splitext must be called on that lowered name โ€” both mocks
+    must reflect that same flow.
+    """
+    lowered = original_filename.lower()       # e.g. "model.vtkhdf"
+    stem, ext = lowered.rsplit(".", 1)        # "model", "vtkhdf"
+    with patch("os.path.exists", return_value=True), \
+         patch("os.path.basename", return_value=lowered), \
+         patch("os.path.splitext", return_value=(stem, f".{ext}")), \
+         patch("ansys.visor.viewer.vtk.io.file_to_dataset.vtkHDFReader") as mock_reader_cls:
+        mock_reader = MagicMock()
+        mock_reader.GetOutput.return_value = vtkPolyData()
+        mock_reader_cls.return_value = mock_reader
+        file_to_dataset(f"/tmp/{original_filename}")
+        mock_reader_cls.assert_called_once()
+
+
+def test_file_to_dataset_vtkhdf_raises_for_unsupported_output_type():
+    """vtkHDFReader returning an unsupported dataset type must raise RuntimeError.
+
+    The source sets is_hdf=True inside the vtkhdf branch, then checks:
+        if is_hdf and not isinstance(output, (vtkDataSet, vtkCompositeDataSet))
+    A plain Python object satisfies `not isinstance(..., vtkDataSet/vtkCompositeDataSet)`
+    because VTK's metaclass only accepts real VTK objects in isinstance โ€” for a
+    non-VTK type the check returns False cleanly (no TypeError).
+    """
+    file_path = "/tmp/model.vtkhdf"
+
+    class _NotAVtkType:
+        pass
+
+    with patch("os.path.exists", return_value=True), \
+         patch("os.path.basename", return_value="model.vtkhdf"), \
+         patch("os.path.splitext", return_value=("model", ".vtkhdf")), \
+         patch("ansys.visor.viewer.vtk.io.file_to_dataset.vtkHDFReader") as mock_reader_cls:
+        mock_reader = MagicMock()
+        mock_reader.GetOutput.return_value = _NotAVtkType()
+        mock_reader_cls.return_value = mock_reader
+        with pytest.raises(RuntimeError, match="unsupported dataset type"):
+            file_to_dataset(file_path)
+
+
+def test_file_to_dataset_vtkhdf_uses_hdf_reader_not_xml_reader():
+    """Ensure .vtkhdf files are dispatched to vtkHDFReader, not any XML reader."""
+    file_path = "/tmp/model.vtkhdf"
+    with patch("os.path.exists", return_value=True), \
+         patch("os.path.basename", return_value="model.vtkhdf"), \
+         patch("os.path.splitext", return_value=("model", ".vtkhdf")), \
+         patch("ansys.visor.viewer.vtk.io.file_to_dataset.vtkHDFReader") as mock_hdf, \
+         patch("ansys.visor.viewer.vtk.io.file_to_dataset.vtkXMLPolyDataReader") as mock_xml_vtp, \
+         patch("ansys.visor.viewer.vtk.io.file_to_dataset.vtkXMLUnstructuredGridReader") as mock_xml_vtu, \
+         patch("ansys.visor.viewer.vtk.io.file_to_dataset.vtkXMLMultiBlockDataReader") as mock_xml_vtm:
+        mock_reader = MagicMock()
+        mock_reader.GetOutput.return_value = vtkPolyData()
+        mock_hdf.return_value = mock_reader
+        file_to_dataset(file_path)
+        mock_hdf.assert_called_once()
+        mock_xml_vtp.assert_not_called()
+        mock_xml_vtu.assert_not_called()
+        mock_xml_vtm.assert_not_called()
+
diff --git a/tests/unit/vtk/io/test_visor_file_io.py b/tests/unit/vtk/io/test_visor_file_io.py
new file mode 100644
index 00000000..fe1873ac
--- /dev/null
+++ b/tests/unit/vtk/io/test_visor_file_io.py
@@ -0,0 +1,542 @@
+import json
+from pathlib import Path
+from unittest.mock import MagicMock, mock_open, patch
+
+import pytest
+from vtkmodules.vtkCommonDataModel import vtkPolyData
+
+from ansys.visor.viewer.core.metadata import ExtendedMetadata, Metadata
+from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState
+from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1
+from ansys.visor.viewer.vtk.io.visor_file_io import _STATE_FILE_NAME, VisorFileIO, _try_unlink
+
+# ------------------------------------------------------------------ #
+# Fixtures
+# ------------------------------------------------------------------ #
+
+@pytest.fixture
+def file_io():
+    """Provides a VisorFileIO instance for testing."""
+    return VisorFileIO()
+
+
+@pytest.fixture
+def minimal_state():
+    """Provides a valid PersistedViewerStateV1 with default fields."""
+    return PersistedViewerStateV1()
+
+
+# ================================================================== #
+# read_dataset
+# ================================================================== #
+
+class TestReadDataset:
+    def test_delegates_to_file_to_dataset(self, file_io):
+        """Verify that file_to_dataset delegates to file_to_dataset."""
+        mock_dataset = MagicMock()
+        with patch(
+            "ansys.visor.viewer.vtk.io.visor_file_io.file_to_dataset",
+            return_value=mock_dataset,
+        ) as mock_fn:
+            result = file_io.read_dataset("/some/path/model.vtu")
+            mock_fn.assert_called_once_with("/some/path/model.vtu")
+            assert result is mock_dataset
+
+    def test_propagates_exception_from_file_to_dataset(self, file_io):
+        """Test that propagates exception from file to dataset."""
+        with patch(
+            "ansys.visor.viewer.vtk.io.visor_file_io.file_to_dataset",
+            side_effect=RuntimeError("bad file"),
+        ):
+            with pytest.raises(RuntimeError, match="bad file"):
+                file_io.read_dataset("/missing/model.vtu")
+
+
+# ================================================================== #
+# read_metadata
+# ================================================================== #
+
+class TestReadMetadata:
+    def test_parses_json_file_into_metadata(self, file_io):
+        """Verify that read_metadata parses json file into Metadata."""
+        metadata_dict = {"name": "my_model", "unit": "m"}
+        with patch("builtins.open", mock_open(read_data=json.dumps(metadata_dict).encode())):
+            result = file_io.read_metadata("/some/meta.json")
+        assert isinstance(result, Metadata)
+        assert result.name == "my_model"
+        assert result.unit == "m"
+
+    def test_opens_file_in_binary_mode(self, file_io):
+        """Verify that read_metadata opens file in binary mode."""
+        metadata_dict = {"name": "x", "unit": ""}
+        m = mock_open(read_data=json.dumps(metadata_dict).encode())
+        with patch("builtins.open", m):
+            file_io.read_metadata("/some/meta.json")
+        m.assert_called_once_with("/some/meta.json", "rb")
+
+    def test_propagates_file_not_found(self, file_io):
+        """Verify that read_metadata propagates FileNotFoundError."""
+        with patch("builtins.open", side_effect=FileNotFoundError("no file")):
+            with pytest.raises(FileNotFoundError):
+                file_io.read_metadata("/missing/meta.json")
+
+    def test_propagates_invalid_json(self, file_io):
+        """Verify that read_metadata propagates JSONDecodeError on invalid json."""
+        with patch("builtins.open", mock_open(read_data=b"not json")):
+            with pytest.raises(json.JSONDecodeError):
+                file_io.read_metadata("/bad/meta.json")
+
+
+# ================================================================== #
+# get_state_file_path
+# ================================================================== #
+
+class TestGetStateFilePath:
+    def test_returns_visor_json_under_state_dir(self, file_io):
+        """Verify that get_state_file_path returns visor.json under state dir."""
+        result = file_io.get_state_file_path("/some/dir")
+        assert result == Path("/some/dir") / _STATE_FILE_NAME
+
+    def test_accepts_trailing_slash(self, file_io):
+        """Verify that get_state_file_path accepts trailing slash in directory path."""
+        result = file_io.get_state_file_path("/some/dir/")
+        assert result == Path("/some/dir") / _STATE_FILE_NAME
+
+    def test_returns_path_object(self, file_io):
+        """Verify that get_state_file_path returns a Path object."""
+        result = file_io.get_state_file_path("/any/dir")
+        assert isinstance(result, Path)
+
+
+# ================================================================== #
+# write_state
+# ================================================================== #
+
+class TestWriteState:
+    def test_creates_directory_if_absent(self, file_io, minimal_state):
+        """Verify that write_state creates directory if absent."""
+        m = mock_open()
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.mkdir") as mock_mkdir, \
+             patch("builtins.open", m):
+            file_io.write_state("/new/dir", minimal_state)
+        mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
+
+    def test_writes_json_to_state_file(self, file_io, minimal_state):
+        """Verify that write_state writes valid JSON to state file."""
+        m = mock_open()
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.mkdir"), \
+             patch("builtins.open", m):
+            file_io.write_state("/some/dir", minimal_state)
+        handle = m()
+        written = "".join(call.args[0] for call in handle.write.call_args_list)
+        # Must be valid JSON and round-trip back to the same state
+        parsed = PersistedViewerStateV1.model_validate_json(written)
+        assert parsed.version == minimal_state.version
+
+    def test_opens_file_in_write_mode(self, file_io, minimal_state):
+        """Verify that write_state opens file in write mode."""
+        m = mock_open()
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.mkdir"), \
+             patch("builtins.open", m):
+            file_io.write_state("/some/dir", minimal_state)
+        expected_path = Path("/some/dir") / _STATE_FILE_NAME
+        m.assert_called_once_with(expected_path, "w")
+
+    def test_returns_path_to_written_file(self, file_io, minimal_state):
+        """Verify that write_state returns path to written file."""
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.mkdir"), \
+             patch("builtins.open", mock_open()):
+            result = file_io.write_state("/some/dir", minimal_state)
+        assert result == Path("/some/dir") / _STATE_FILE_NAME
+
+
+# ================================================================== #
+# read_state
+# ================================================================== #
+
+class TestReadState:
+    def test_returns_persisted_state_from_valid_json(self, file_io, minimal_state):
+        """Verify that read_state returns persisted state."""
+        json_bytes = minimal_state.model_dump_json()
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.exists", return_value=True), \
+             patch("builtins.open", mock_open(read_data=json_bytes)):
+            result = file_io.read_state("/some/dir")
+        assert isinstance(result, PersistedViewerStateV1)
+        assert result.version == minimal_state.version
+
+    def test_raises_file_not_found_when_state_file_missing(self, file_io):
+        """Verify that read_state raises FileNotFoundError when state file is missing."""
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.exists", return_value=False):
+            with pytest.raises(FileNotFoundError, match="State file not found"):
+                file_io.read_state("/missing/dir")
+
+    def test_opens_file_in_read_mode(self, file_io, minimal_state):
+        """Verify that read_state opens file in read mode."""
+        json_bytes = minimal_state.model_dump_json()
+        m = mock_open(read_data=json_bytes)
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.exists", return_value=True), \
+             patch("builtins.open", m):
+            file_io.read_state("/some/dir")
+        expected_path = Path("/some/dir") / _STATE_FILE_NAME
+        m.assert_called_once_with(expected_path, "r")
+
+    def test_propagates_invalid_json(self, file_io):
+        """Verify that read_state propagates exception on invalid json."""
+        with patch("ansys.visor.viewer.vtk.io.visor_file_io.Path.exists", return_value=True), \
+             patch("builtins.open", mock_open(read_data="not json")):
+            with pytest.raises(Exception):
+                file_io.read_state("/some/dir")
+
+
+# ================================================================== #
+# resolve_input
+# ================================================================== #
+
+class TestResolveInput:
+    def test_returns_none_when_input_is_none(self, file_io):
+        """Verify that resolve_input returns None when input is None."""
+        assert file_io.resolve_input(None) is None
+
+    def test_returns_dataset_unchanged_when_vtk_object(self, file_io):
+        """Verify that resolve_input returns dataset unchanged when vtk object."""
+        dummy = vtkPolyData()
+        assert file_io.resolve_input(dummy) is dummy
+
+    def test_reads_dataset_from_disk_when_str(self, file_io):
+        """Verify that resolve_input reads dataset from disk when input is str."""
+        mock_dataset = MagicMock()
+        with patch.object(file_io, "read_dataset", return_value=mock_dataset) as mock_read:
+            result = file_io.resolve_input("/some/model.vtu")
+            mock_read.assert_called_once_with("/some/model.vtu")
+            assert result is mock_dataset
+
+    def test_raises_type_error_on_unsupported_type(self, file_io):
+        """Verify that resolve_input raises TypeError on unsupported type."""
+        with pytest.raises(TypeError, match="Unsupported input type"):
+            file_io.resolve_input(12345)
+
+
+# ================================================================== #
+# write_dataset
+# ================================================================== #
+
+class TestWriteDataset:
+    def test_delegates_to_dataset_to_file(self, file_io, tmp_path):
+        """Verify that write_dataset delegates to dataset_to_file."""
+        dest = tmp_path / "snap.vtkhdf"
+        dataset = MagicMock()
+        with patch(
+            "ansys.visor.viewer.vtk.io.visor_file_io.dataset_to_file",
+            return_value=dest,
+        ) as mock_fn:
+            result = file_io.write_dataset(dest, dataset)
+            mock_fn.assert_called_once_with(dest, dataset)
+            assert result == dest
+
+    def test_returns_path_from_dataset_to_file(self, file_io, tmp_path):
+        """Verify that write_dataset returns path from dataset_to_file."""
+        dest = tmp_path / "snap.vtkhdf"
+        with patch(
+            "ansys.visor.viewer.vtk.io.visor_file_io.dataset_to_file",
+            return_value=dest,
+        ):
+            assert file_io.write_dataset(dest, MagicMock()) == dest
+
+
+# ================================================================== #
+# resolve_metadata
+# ================================================================== #
+
+
+class TestResolveMetadata:
+    def test_returns_default_metadata_when_none_and_no_input(self, file_io):
+        """Verify that resolve_metadata returns default metadata when no input is provided."""
+        result = file_io.resolve_metadata(None, None)
+        assert isinstance(result, Metadata)
+        assert result.name == "model"
+        assert result.unit == ""
+
+    def test_infers_name_from_file_path_when_metadata_is_none(self, file_io):
+        """Verify that resolve_metadata infers name from file path when metadata is None."""
+        result = file_io.resolve_metadata(None, "/some/path/my_dataset.vtp")
+        assert result.name == "my_dataset"
+
+    def test_infers_name_from_vtkhdf_file_path(self, file_io):
+        """Verify that resolve_metadata infers name from vtkhdf file path."""
+        result = file_io.resolve_metadata(None, "/some/path/mesh_snapshot.vtkhdf")
+        assert result.name == "mesh_snapshot"
+
+    def test_falls_back_to_model_when_input_is_not_str(self, file_io):
+        """Verify that resolve_metadata falls back to 'model' when input is not str."""
+        result = file_io.resolve_metadata(None, vtkPolyData())
+        assert result.name == "model"
+
+    def test_returns_metadata_instance_unchanged(self, file_io):
+        """Verify that resolve_metadata returns Metadata instance unchanged."""
+        dummy = Metadata(name="test", unit="m")
+        assert file_io.resolve_metadata(dummy, None) is dummy
+
+    def test_reads_metadata_from_disk_when_str(self, file_io):
+        """Verify that resolve_metadata reads metadata from disk when input is str."""
+        mock_metadata = Metadata(name="from_file", unit="mm")
+        with patch.object(file_io, "read_metadata", return_value=mock_metadata) as mock_read:
+            result = file_io.resolve_metadata("/some/meta.json", None)
+            mock_read.assert_called_once_with("/some/meta.json")
+            assert result is mock_metadata
+
+    def test_raises_type_error_on_unsupported_type(self, file_io):
+        """Verify that resolve_metadata raises TypeError on unsupported type."""
+        with pytest.raises(TypeError, match="Unsupported metadata type"):
+            file_io.resolve_metadata(42, None)
+
+
+# ================================================================== #
+# resolve
+# ================================================================== #
+
+class TestResolve:
+    def test_returns_none_dataset_and_default_metadata_when_both_none(self, file_io):
+        """Verify that resolve returns None dataset and default metadata when both inputs are None."""
+        dataset, ext_meta = file_io.resolve(None, None)
+        assert dataset is None
+        assert isinstance(ext_meta, ExtendedMetadata)
+        assert ext_meta.name == "model"
+        assert ext_meta.file_path is None
+        assert ext_meta.metadata_path is None
+
+    def test_sets_file_path_when_input_is_str(self, file_io):
+        """Verify that resolve() sets file_path when input is str."""
+        mock_dataset = MagicMock()
+        with patch.object(file_io, "resolve_input", return_value=mock_dataset), \
+             patch.object(file_io, "resolve_metadata", return_value=Metadata(name="m", unit="")):
+            _, ext_meta = file_io.resolve("/path/model.vtu", None)
+        assert ext_meta.file_path == "/path/model.vtu"
+
+    def test_does_not_set_file_path_when_input_is_not_str(self, file_io):
+        """Verify that resolve() does not set file_path when input is not str."""
+        dummy = vtkPolyData()
+        with patch.object(file_io, "resolve_metadata", return_value=Metadata(name="m", unit="")):
+            _, ext_meta = file_io.resolve(dummy, None)
+        assert ext_meta.file_path is None
+
+    def test_sets_metadata_path_when_metadata_is_str(self, file_io):
+        """Verify that resolve() sets metadata_path when metadata is str."""
+        mock_metadata = Metadata(name="m", unit="")
+        with patch.object(file_io, "resolve_input", return_value=None), \
+             patch.object(file_io, "resolve_metadata", return_value=mock_metadata):
+            _, ext_meta = file_io.resolve(None, "/path/meta.json")
+        assert ext_meta.metadata_path == "/path/meta.json"
+
+    def test_does_not_set_metadata_path_when_metadata_is_not_str(self, file_io):
+        """Verify that resolve() does not set metadata_path when metadata is not str."""
+        dummy_meta = Metadata(name="m", unit="")
+        with patch.object(file_io, "resolve_input", return_value=None):
+            _, ext_meta = file_io.resolve(None, dummy_meta)
+        assert ext_meta.metadata_path is None
+
+    def test_returns_extended_metadata_type(self, file_io):
+        """Verify that resolve() returns ExtendedMetadata type."""
+        _, ext_meta = file_io.resolve(None, None)
+        assert isinstance(ext_meta, ExtendedMetadata)
+
+    def test_delegates_to_resolve_input_and_resolve_metadata(self, file_io):
+        """Verify that resolve() delegates to resolve_input and resolve_metadata."""
+        mock_dataset = MagicMock()
+        mock_metadata = Metadata(name="x", unit="y")
+        with patch.object(file_io, "resolve_input", return_value=mock_dataset) as mock_ri, \
+             patch.object(file_io, "resolve_metadata", return_value=mock_metadata) as mock_rm:
+            dataset, _ = file_io.resolve("input.vtu", "meta.json")
+            mock_ri.assert_called_once_with("input.vtu")
+            mock_rm.assert_called_once_with("meta.json", "input.vtu")
+            assert dataset is mock_dataset
+
+
+# ================================================================== #
+# build_metadata_for_load_state
+# ================================================================== #
+
+class TestBuildMetadataForLoadState:
+    def _make_state(self, **kwargs) -> PersistedDatasetState:
+        return PersistedDatasetState(**kwargs)
+
+    def test_returns_extended_metadata(self, file_io):
+        """Verify that build_metadata_for_load_state returns ExtendedMetadata."""
+        ds_state = self._make_state()
+        result = file_io.build_metadata_for_load_state("my_model", "m", ds_state)
+        assert isinstance(result, ExtendedMetadata)
+
+    def test_name_and_unit_are_set_from_arguments(self, file_io):
+        """Verify that name and unit are set from arguments."""
+        ds_state = self._make_state()
+        result = file_io.build_metadata_for_load_state("my_model", "mm", ds_state)
+        assert result.name == "my_model"
+        assert result.unit == "mm"
+
+    def test_none_unit_becomes_empty_string(self, file_io):
+        """Verify that None unit becomes empty string."""
+        ds_state = self._make_state()
+        result = file_io.build_metadata_for_load_state("model", None, ds_state)
+        assert result.unit == ""
+
+    def test_file_path_comes_from_source_file_path(self, file_io):
+        """Verify that file path comes from source file path."""
+        ds_state = self._make_state(source_file_path="/orig/mesh.vtu")
+        result = file_io.build_metadata_for_load_state("mesh", "m", ds_state)
+        assert result.file_path == "/orig/mesh.vtu"
+
+    def test_metadata_path_comes_from_source_metadata_path(self, file_io):
+        """Verify that metadata path comes from source metadata path."""
+        ds_state = self._make_state(source_metadata_path="/orig/meta.json")
+        result = file_io.build_metadata_for_load_state("mesh", "m", ds_state)
+        assert result.metadata_path == "/orig/meta.json"
+
+    def test_file_path_is_none_when_source_absent(self, file_io):
+        """Verify that file path is None when source file path is absent."""
+        ds_state = self._make_state()
+        result = file_io.build_metadata_for_load_state("model", "m", ds_state)
+        assert result.file_path is None
+
+    def test_state_is_the_persisted_dataset_state(self, file_io):
+        """Verify that state is the PersistedDatasetState."""
+        ds_state = self._make_state(serialized_dataset_path="/save/snap.vtkhdf")
+        result = file_io.build_metadata_for_load_state("model", "m", ds_state)
+        assert result.state is ds_state
+        assert result.state.serialized_dataset_path == "/save/snap.vtkhdf"
+
+    def test_all_fields_populated_together(self, file_io):
+        """Verify that all fields are populated together."""
+        ds_state = self._make_state(
+            source_file_path="/orig/mesh.vtu",
+            source_metadata_path="/orig/meta.json",
+            serialized_dataset_path="/save/mesh_snapshot.vtkhdf",
+        )
+        result = file_io.build_metadata_for_load_state("mesh", "mm", ds_state)
+        assert result.name == "mesh"
+        assert result.unit == "mm"
+        assert result.file_path == "/orig/mesh.vtu"
+        assert result.metadata_path == "/orig/meta.json"
+        assert result.state.serialized_dataset_path == "/save/mesh_snapshot.vtkhdf"
+
+
+# ================================================================== #
+# _try_unlink
+# ================================================================== #
+
+class TestTryUnlink:
+    def test_deletes_file_and_returns_true(self, tmp_path):
+        """Verify that _try_unlink deletes file and returns True."""
+        f = tmp_path / "snap.vtkhdf"
+        f.touch()
+        assert _try_unlink(f) is True
+        assert not f.exists()
+
+    def test_returns_false_on_permission_error(self, tmp_path):
+        """Verify that _try_unlink returns False on PermissionError."""
+        f = tmp_path / "snap.vtkhdf"
+        f.touch()
+        with patch.object(Path, "unlink", side_effect=PermissionError("locked")):
+            assert _try_unlink(f) is False
+
+    def test_logs_warning_on_permission_error(self, tmp_path):
+        """Verify that _try_unlink logs warning on PermissionError."""
+        f = tmp_path / "snap.vtkhdf"
+        f.touch()
+        with patch.object(Path, "unlink", side_effect=PermissionError("locked")), \
+             patch("ansys.visor.viewer.vtk.io.visor_file_io.logger") as mock_log:
+            _try_unlink(f)
+            mock_log.warning.assert_called_once()
+
+    def test_propagates_other_errors(self, tmp_path):
+        """Verify that _try_unlink propagates other errors."""
+        f = tmp_path / "snap.vtkhdf"
+        f.touch()
+        with patch.object(Path, "unlink", side_effect=OSError("disk full")):
+            with pytest.raises(OSError, match="disk full"):
+                _try_unlink(f)
+
+
+# ================================================================== #
+# delete_stale_snapshots
+# ================================================================== #
+
+class TestDeleteStaleSnapshots:
+    def _make_state(self, dataset_states: dict) -> PersistedViewerStateV1:
+        from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
+        return PersistedViewerStateV1.from_components(
+            ui_state=VisorUIState(), unit="m", orthographic_enabled=None,
+            cross_section_enabled=None, edges_enabled=None, bounding_box_enabled=None,
+            datasets=dataset_states
+        )
+
+    def test_deletes_vtkhdf_not_in_state(self, file_io, tmp_path):
+        """Verify that make_state deletes vtkhdf not in state."""
+        stale = tmp_path / "old_snapshot.vtkhdf"
+        stale.touch()
+        state = self._make_state({})  # no datasets ??? no kept paths
+
+        deleted = file_io.delete_stale_snapshots(str(tmp_path), state)
+
+        assert not stale.exists()
+        assert stale in deleted
+
+    def test_keeps_vtkhdf_referenced_in_state(self, file_io, tmp_path):
+        """Verify that make_state keeps vtkhdf referenced in state."""
+        kept = tmp_path / "plate_snapshot.vtkhdf"
+        kept.touch()
+        ds_state = PersistedDatasetState(serialized_dataset_path=str(kept))
+        state = self._make_state({"plate": ds_state})
+
+        deleted = file_io.delete_stale_snapshots(str(tmp_path), state)
+
+        assert kept.exists()
+        assert kept not in deleted
+
+    def test_deletes_stale_and_keeps_current_simultaneously(self, file_io, tmp_path):
+        """Verify that make_state deletes stale and keeps current simultaneously."""
+        stale = tmp_path / "removed_snapshot.vtkhdf"
+        current = tmp_path / "plate_snapshot.vtkhdf"
+        stale.touch()
+        current.touch()
+        ds_state = PersistedDatasetState(serialized_dataset_path=str(current))
+        state = self._make_state({"plate": ds_state})
+
+        deleted = file_io.delete_stale_snapshots(str(tmp_path), state)
+
+        assert not stale.exists()
+        assert current.exists()
+        assert stale in deleted
+        assert current not in deleted
+
+    def test_does_not_delete_non_vtkhdf_files(self, file_io, tmp_path):
+        """Verify that make_state does not delete non vtkhdf files."""
+        other = tmp_path / "visor.json"
+        other.touch()
+        state = self._make_state({})
+
+        file_io.delete_stale_snapshots(str(tmp_path), state)
+
+        assert other.exists()
+
+    def test_returns_empty_list_when_nothing_to_delete(self, file_io, tmp_path):
+        """Verify that make_state returns empty list when nothing to delete."""
+        kept = tmp_path / "plate_snapshot.vtkhdf"
+        kept.touch()
+        ds_state = PersistedDatasetState(serialized_dataset_path=str(kept))
+        state = self._make_state({"plate": ds_state})
+
+        deleted = file_io.delete_stale_snapshots(str(tmp_path), state)
+
+        assert deleted == []
+
+    def test_ignores_dataset_state_with_no_snapshot_path(self, file_io, tmp_path):
+        """Verify that make_state ignores dataset state with no snapshot path."""
+        stale = tmp_path / "old_snapshot.vtkhdf"
+        stale.touch()
+        ds_state = PersistedDatasetState(serialized_dataset_path=None)
+        state = self._make_state({"plate": ds_state})
+
+        deleted = file_io.delete_stale_snapshots(str(tmp_path), state)
+
+        assert stale not in [p for p in [stale] if stale.exists()]
+        assert stale in deleted  # None path doesn't protect anything
+
diff --git a/tests/unit/vtk/scene/__init__.py b/tests/unit/vtk/scene/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/vtk/scene/test_base.py b/tests/unit/vtk/scene/test_base.py
new file mode 100644
index 00000000..39a3dc76
--- /dev/null
+++ b/tests/unit/vtk/scene/test_base.py
@@ -0,0 +1,28 @@
+"""Tests for VisorSceneBase as an abstract class.
+
+These tests only assert the abstract contract of VisorSceneBase:
+  1. Direct instantiation raises TypeError.
+  2. Exactly the expected set of abstract methods is declared.
+
+Behavioural coverage of shared logic lives in test_local_scene.py, exercised
+through the concrete VisorLocalScene subclass.
+"""
+from ansys.visor.viewer.vtk.scene.base import VisorSceneBase
+
+
+def test_cannot_instantiate_directly():
+    """VisorSceneBase is abstract and must raise TypeError on direct instantiation."""
+    try:
+        VisorSceneBase(None)  # type: ignore[abstract]
+        raise AssertionError("Expected TypeError was not raised")
+    except TypeError:
+        pass
+
+
+def test_abstract_methods():
+    """VisorSceneBase declares exactly the two expected abstract hooks."""
+    assert VisorSceneBase.__abstractmethods__ == {
+        "_get_runtime_state_async",
+        "_apply_runtime_state_to_render",
+    }
+
diff --git a/tests/unit/vtk/scene/test_local_scene.py b/tests/unit/vtk/scene/test_local_scene.py
new file mode 100644
index 00000000..6ae337ea
--- /dev/null
+++ b/tests/unit/vtk/scene/test_local_scene.py
@@ -0,0 +1,780 @@
+import json
+from types import SimpleNamespace
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer.core.metadata import ExtendedMetadata
+from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import (
+    WasmRendererAnnotation,
+    WasmWidgetHandles,
+)
+from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo
+from ansys.visor.viewer.vtk.scene.local_scene import VisorLocalScene
+
+
+class _DummyPoints:
+    """A lightweight dummy class to simulate VTK points for testing purposes."""
+    def __init__(self, pts):
+        self._pts = pts
+
+    def GetPoint(self, i): # noqa: N802
+        return tuple(self._pts[i])
+
+
+class _DummyCell:
+    """A lightweight dummy class to simulate VTK cells for testing purposes."""
+    def __init__(self, points):
+        self._points = points
+        self._pts = _DummyPoints(points)
+
+    def GetNumberOfPoints(self): # noqa: N802
+        return len(self._points)
+
+    def GetPoints(self): # noqa: N802
+        return self._pts
+
+
+class _DummyDataset:
+    """A lightweight dummy class to simulate VTK datasets for testing purposes."""
+    def __init__(self, cells):
+        self._cells = cells
+
+    def GetNumberOfCells(self): # noqa: N802
+        return len(self._cells)
+
+    def GetCell(self, idx): # noqa: N802
+        if 0 <= idx < len(self._cells):
+            return self._cells[idx]
+        return None
+
+
+class _DummyMapper:
+    """A lightweight dummy class to simulate VTK mappers for testing purposes."""
+    def __init__(self, dataset):
+        self._dataset = dataset
+
+    def GetInputDataObject(self, a, b): # noqa: N802
+        return self._dataset
+
+
+class _DummyActor:
+    """A lightweight dummy class to simulate VTK actors for testing purposes."""
+    def __init__(self, mapper):
+        self._mapper = mapper
+
+    def GetMapper(self): # noqa: N802
+        return self._mapper
+
+
+class _DummyActors:
+    """A lightweight dummy class to simulate VTK actor collections for testing purposes."""
+    def __init__(self, actors):
+        self._actors = actors
+        self._idx = 0
+
+    def InitTraversal(self): # noqa: N802
+        self._idx = 0
+
+    def GetNextActor(self): # noqa: N802
+        if self._idx >= len(self._actors):
+            return None
+        a = self._actors[self._idx]
+        self._idx += 1
+        return a
+
+
+@pytest.fixture
+def pipeline_instance():
+    """Return a fully mocked VisorLocalScene instance with no VTK or Trame dependencies.
+
+    All VTK infrastructure lives inside VisorLocalRenderer; we inject a plain
+    MagicMock so the coordinator logic is exercised in isolation.
+    """
+    mock_server = MagicMock()
+    mock_server.state = {}
+
+    mock_renderer = MagicMock()  # stands in for IRenderer / VisorLocalRenderer
+    # Real empty dict so SceneGraphStateBuilder's ``pipelines.get(node.id)``
+    # returns None
+    mock_renderer.pipelines = {}
+    # build_renderer_annotation() must return a real
+    # RendererAnnotation instance, not a MagicMock, since it is validated by
+    # RuntimeVTKInfo.renderer_annotation.
+    mock_renderer.build_renderer_annotation.return_value = WasmRendererAnnotation(
+        nodes={},
+        widgets=WasmWidgetHandles(
+            orientation_widget_id=10,
+            cross_section_plane_id=20,
+            cross_section_plane_widget_id=21,
+            cross_section_plane_representation_id=22,
+            bounding_box_algorithm_id=30,
+            bounding_box_outline_actor_id=31,
+            bounding_box_axes_actor_id=32,
+        ),
+    )
+    # frontend_ref_name must be a real str, since it is passed to
+    # VisorFrontendBridge's constructor.
+    mock_renderer.frontend_ref_name = "test-ref-name"
+
+    with patch("ansys.visor.viewer.renderer.local_renderer.VisorLocalRenderer",
+               return_value=mock_renderer), \
+         patch("ansys.visor.viewer.vtk.scene.base.VisorSceneBase._initialize_dataset_registry",
+               return_value=MagicMock()), \
+         patch("ansys.visor.viewer.vtk.scene.base.VisorSceneBase._initialize_scene_graph",
+               lambda self: setattr(self, "_scene_graph", MagicMock())), \
+         patch("ansys.visor.viewer.vtk.scene.base.VisorStateMapper"), \
+         patch("ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorFrontendBridge") as mock_bridge_cls:
+        instance = VisorLocalScene(mock_server)
+        # Exposed so tests can assert on how VisorFrontendBridge was constructed.
+        instance._test_frontend_bridge_cls_mock = mock_bridge_cls
+
+        # Mock dataset registry API
+        instance._dataset_registry = MagicMock()
+        instance._dataset_registry.datasets = {}
+        instance._dataset_registry.unit = ""
+        instance._dataset_registry._update_unit = MagicMock()
+        instance._dataset_registry.get_sanitized_metadata_name = MagicMock(
+            side_effect=lambda m: getattr(m, "name", "dataset")
+        )
+        instance._dataset_registry.add = MagicMock()
+        instance._dataset_registry.remove = MagicMock()
+        instance._dataset_registry.clear = MagicMock()
+        instance._dataset_registry.list_info = MagicMock(return_value={})
+        instance._dataset_registry.list_variables = MagicMock(return_value=[])
+        instance._dataset_registry.update_variables = MagicMock()
+        instance._dataset_registry.count = 0
+        instance._dataset_registry.runtime_state_dict = {}
+
+        # Mock scene graph with required APIs
+        instance._scene_graph.load_dataset.return_value = 123
+        instance._scene_graph.remove_dataset = MagicMock()
+        instance._scene_graph.get_part_name_to_id_map = MagicMock(return_value={})
+        instance._scene_graph.get_descendant_part_nodes.return_value = []
+        instance._scene_graph.descendant_part_count.return_value = 0
+        dataset_node = MagicMock()
+        # Default: no leaves. Tests that exercise pipeline lifecycle override
+        # this and configure ``instance._renderer.pipelines`` / assert on
+        # ``instance._renderer.register_node`` / ``deregister_node``.
+        dataset_node.get_descendant_part_nodes.return_value = []
+        # Accept include_self kwarg (used by add_dataset's per-leaf register loop).
+        instance._scene_graph.get_descendant_node.side_effect = (
+            lambda dataset_id, include_self=False: dataset_node if dataset_id == 123 else None
+        )
+        instance._scene_graph.state = SceneGraphNodeInfo(
+            id=1, name="root", isActorNode=False, isGroupNode=True, nodeType="group",
+        )
+        instance._scene_graph.is_part_node = False
+        instance._scene_graph.is_group_node = True
+        instance._scene_graph.children = []
+        instance._scene_graph.Bounds = [0, 1, 0, 1, 0, 1]
+
+
+        yield instance
+
+        # Teardown: clear state to avoid leaks between tests
+        try:
+            instance._dataset_registry.clear()
+        except Exception:
+            pass
+        instance._scene_graph = None
+
+
+def dummy_dataset():
+    """Return a dummy dataset for testing purposes."""
+    return MagicMock()
+
+def test_initialization(pipeline_instance):
+    """Test pipeline initialization."""
+    assert pipeline_instance is not None
+    assert pipeline_instance._dataset_registry.datasets == {}
+    assert pipeline_instance._dataset_registry.unit == ""
+    assert pipeline_instance._renderer is not None
+    assert pipeline_instance._renderer._render_window is not None
+    assert pipeline_instance._renderer._render_window_interactor is not None
+    assert pipeline_instance._server is not None
+    assert pipeline_instance._scene_graph is not None
+
+def test_init_passes_frontend_ref_name_to_bridge(pipeline_instance):
+    """VisorFrontendBridge is constructed with the renderer's frontend_ref_name
+    string, not the renderer or a view object."""
+    mock_bridge_cls = pipeline_instance._test_frontend_bridge_cls_mock
+    mock_bridge_cls.assert_called_once_with(
+        pipeline_instance._server, "test-ref-name"
+    )
+    args, _ = mock_bridge_cls.call_args
+    assert isinstance(args[1], str)
+
+
+def test_cleanup_state(pipeline_instance):
+    """Test that cleanup_state removes wasm_ids and wasm_ref_name from server state."""
+    pipeline_instance._server.state["wasm_ids"] = {"dummy": 1}
+    pipeline_instance._server.state["wasm_ref_name"] = {"dummy": 2}
+    pipeline_instance.cleanup_state()
+    assert "wasm_ids" not in pipeline_instance._server.state
+    assert "wasm_ref_name" not in pipeline_instance._server.state
+
+def test_update_widgets_without_scene_graph(pipeline_instance):
+    """Test that update_widgets without scene graph raises RuntimeError."""
+    pipeline_instance._scene_graph = None
+    with pytest.raises(RuntimeError):
+        pipeline_instance.update_widgets()
+
+def test_reset_camera_no_scene_graph(pipeline_instance):
+    """Test that resetting camera with no scene graph does not."""
+    pipeline_instance._scene_graph = None
+    pipeline_instance.reset_camera()  # Should not raise
+
+def test_render(pipeline_instance):
+    """Test render() does not raise."""
+    dataset = dummy_dataset()
+    metadata = ExtendedMetadata(name="test_model", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    pipeline_instance.render()  # Should not raise
+
+def test_remove_dataset_error(pipeline_instance):
+    """Test that remove_dataset raises RuntimeError."""
+    pipeline_instance._scene_graph = None
+    with pytest.raises(RuntimeError):
+        pipeline_instance.remove_dataset(1)
+
+def test_add_dataset_none_input(pipeline_instance):
+    """Test that add_dataset with None input raises RuntimeError."""
+    with pytest.raises(ValueError):
+        pipeline_instance.add_dataset(None, ExtendedMetadata(name="test", unit="m"))
+
+def test_visorscene_constructor_runtimeerror():
+    """Test that VisorLocalScene constructor raises RuntimeError when encountering an error during initialization."""
+    mock_server = MagicMock()
+    mock_server.state = {}
+
+    with patch("ansys.visor.viewer.renderer.local_renderer.VisorLocalRenderer",
+               side_effect=RuntimeError("Init error")):
+        with pytest.raises(RuntimeError, match="Failed to initialize VTK pipeline: Init error"):
+            VisorLocalScene(mock_server)
+
+def test_get_scene_details_none_scene_graph(pipeline_instance):
+    """Test that get_scene_details raises RuntimeError when scene graph is None."""
+    pipeline_instance._scene_graph = None
+    with patch.object(pipeline_instance, "_initialize_scene_graph", side_effect=RuntimeError("Scene graph is not initialized.")):
+        with pytest.raises(RuntimeError, match="Scene graph is not initialized."):
+            pipeline_instance.get_scene_details()
+
+def test_clear_hits_remove_actor(pipeline_instance):
+    """clear() delegates to the renderer's deregister_all()."""
+    pipeline_instance.clear()
+    pipeline_instance._renderer.deregister_all.assert_called_once()
+
+def test_populate_scene_happy_case(pipeline_instance):
+    """populate_scene delegates the widget refresh via update_widgets."""
+    # After I5, actor attach happens per-leaf at add_dataset time; populate_scene
+    # is only responsible for refreshing widget bounds/count via update_widgets.
+    pipeline_instance.update_widgets = MagicMock()
+    pipeline_instance.populate_scene()
+    pipeline_instance.update_widgets.assert_called_once()
+
+def test_populate_scene_triggers_initialize_scene_graph(pipeline_instance):
+    """Test that populate_scene triggers _initialize_scene_graph."""
+    pipeline_instance._scene_graph = None
+    with patch.object(pipeline_instance, "_initialize_scene_graph") as mock_init_scene_graph:
+        # Patch update_widgets to avoid side effects
+        pipeline_instance.update_widgets = MagicMock()
+        pipeline_instance.populate_scene()
+        mock_init_scene_graph.assert_called_once()
+
+def test_add_dataset_triggers_initialize_scene_graph(pipeline_instance):
+    """Test that add_dataset triggers _initialize_scene_graph."""
+    pipeline_instance._scene_graph = None
+    dataset = dummy_dataset()
+    metadata = ExtendedMetadata(name="test_model", unit="m")
+    with patch.object(pipeline_instance, "_initialize_scene_graph") as mock_init_scene_graph:
+        def init_scene_graph_side_effect():
+            pipeline_instance._scene_graph = MagicMock()
+        mock_init_scene_graph.side_effect = init_scene_graph_side_effect
+        pipeline_instance.add_dataset(dataset, metadata)
+        mock_init_scene_graph.assert_called_once()
+
+def test_render_happy_case(pipeline_instance):
+    """Test render() delegates to the renderer backend."""
+    dataset = dummy_dataset()
+    metadata = ExtendedMetadata(name="test_model", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    pipeline_instance.render()
+    pipeline_instance._renderer.render.assert_called_once()
+
+def test_reset_camera_happy_case(pipeline_instance):
+    """Test reset_camera() delegates to the renderer with scene-graph bounds."""
+    dataset = dummy_dataset()
+    metadata = ExtendedMetadata(name="test_model", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+    pipeline_instance.reset_camera()
+    pipeline_instance._renderer.reset_camera.assert_called_once_with(
+        pipeline_instance._scene_graph.bounds
+    )
+
+import contextlib
+
+
+def make_fully_mocked_visorscene():
+    """Helper function to create a fully mocked VisorLocalScene instance for testing purposes."""
+    mock_server = MagicMock()
+    mock_server.state = {}
+
+    with contextlib.ExitStack() as stack:
+        stack.enter_context(
+            patch("ansys.visor.viewer.renderer.local_renderer.VisorLocalRenderer",
+                  return_value=MagicMock())
+        )
+        # _initialize_dataset_registry is NOT patched: the real VisorDatasetRegistry
+        # is used so that registry.remove() actually mutates .datasets (asserted below).
+        stack.enter_context(
+            patch("ansys.visor.viewer.vtk.scene.base.VisorSceneBase._initialize_scene_graph",
+                  lambda self: setattr(self, "_scene_graph", MagicMock()))
+        )
+        stack.enter_context(
+            patch("ansys.visor.viewer.vtk.scene.base.VisorStateMapper")
+        )
+        stack.enter_context(
+            patch("ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorFrontendBridge")
+        )
+        instance = VisorLocalScene(mock_server)
+        instance._scene_graph = MagicMock()
+        return instance
+
+
+def test_add_dataset_registers_single_leaf_with_renderer(pipeline_instance):
+    """add_dataset delegates each leaf pipeline to renderer.register_node.
+
+    After I5, per-leaf pipeline construction and actor-attach are owned by
+    the renderer's register_node; the coordinator invokes it once per leaf
+    at add_dataset time.
+    """
+    leaf = MagicMock()
+    leaf.id = 1
+    leaf.dataset = MagicMock(name="leaf.dataset")
+    dataset_node = pipeline_instance._scene_graph.get_descendant_node(123)
+    dataset_node.get_descendant_part_nodes.return_value = [leaf]
+
+    dataset = dummy_dataset()
+    metadata = ExtendedMetadata(name="test_model", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+
+    pipeline_instance._renderer.register_node.assert_called_once_with(leaf, leaf.dataset)
+
+
+def test_add_dataset_registers_multiple_leaves_with_renderer(pipeline_instance):
+    """add_dataset calls renderer.register_node for every leaf in the subtree."""
+    leaf1 = MagicMock()
+    leaf1.id = 1
+    leaf1.dataset = MagicMock(name="leaf1.dataset")
+    leaf2 = MagicMock()
+    leaf2.id = 2
+    leaf2.dataset = MagicMock(name="leaf2.dataset")
+    dataset_node = pipeline_instance._scene_graph.get_descendant_node(123)
+    dataset_node.get_descendant_part_nodes.return_value = [leaf1, leaf2]
+
+    dataset = dummy_dataset()
+    metadata = ExtendedMetadata(name="test_model", unit="m")
+    pipeline_instance.add_dataset(dataset, metadata)
+
+    assert pipeline_instance._renderer.register_node.call_count == 2
+    registered = [c[0] for c in pipeline_instance._renderer.register_node.call_args_list]
+    assert (leaf1, leaf1.dataset) in registered
+    assert (leaf2, leaf2.dataset) in registered
+
+
+def test_populate_scene_raises_when_scene_graph_not_initialized(pipeline_instance):
+    """populate_scene raises via update_widgets when the scene graph is uninitialized.
+
+    After I5, populate_scene no longer attaches actors (that happens per-leaf
+    at add_dataset time); it only refreshes widgets.  The scene-graph guard
+    now lives inside update_widgets.
+    """
+    # Arrange: ensure scene graph is None and patch initialize to a no-op so it does not set it
+    pipeline_instance._scene_graph = None
+
+    with patch.object(pipeline_instance, "_initialize_scene_graph", return_value=None):
+        # Act / Assert: calling the public method should propagate the RuntimeError from the internal path
+        with pytest.raises(RuntimeError, match="Scene graph has not been initialized, cannot reset widgets and camera."):
+            pipeline_instance.populate_scene()
+
+
+def test_build_scene_graph_state_does_not_stamp_wasm_ids(pipeline_instance):
+    """
+    _build_scene_graph_state no longer stamps renderer handles; it returns
+    pure scene-graph structure. Stamping now happens separately, in
+    get_scene_details, from the renderer's annotation.
+    """
+    from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo
+
+    # A real leaf-info object; the builder must leave it unstamped.
+    leaf_info = SceneGraphNodeInfo(
+        id=555, name="p", isActorNode=True, isGroupNode=False, nodeType="vtkPolyData",
+    )
+    root_info = SceneGraphNodeInfo(
+        id=1, name="root", isActorNode=False, isGroupNode=True, nodeType="root",
+    )
+
+    leaf_node = MagicMock()
+    leaf_node.id = 555
+    leaf_node.is_part_node = True
+    leaf_node.is_group_node = False
+    leaf_node.state = leaf_info
+    pipeline_instance._scene_graph.is_part_node = False
+    pipeline_instance._scene_graph.is_group_node = True
+    pipeline_instance._scene_graph.state = root_info
+    pipeline_instance._scene_graph.children = [leaf_node]
+
+    out = pipeline_instance._build_scene_graph_state()
+
+    assert out is root_info
+    assert len(out.children) == 1
+    stamped = out.children[0]
+    assert stamped is leaf_info
+
+def _extract_name_from_dataset_value(val):
+    """Helper function to extract the name from a dataset value, which may be an object with
+    a .name attribute or a dict with a "name" key.
+    Returns the name if found, otherwise returns None.
+    """
+    # The pipeline stores dataset metadata either as an object with .name or as a dict
+    if hasattr(val, "name"):
+        return val.name
+    if isinstance(val, dict):
+        return val.get("name")
+    return None
+
+def test_remove_dataset_not_found_logs_and_raises(pipeline_instance):
+    """
+    When the scene graph does not contain the dataset id, remove_dataset()
+    should log an error and raise a ValueError with the expected message.
+    Only the public method is called and we mock the module logger.
+    """
+    missing_id = 999
+    # Ensure scene graph will report the node as missing
+    pipeline_instance._scene_graph.get_descendant_node.return_value = None
+    pipeline_instance._scene_graph.remove_dataset = MagicMock()
+
+    with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger:
+        with pytest.raises(ValueError, match=f"Dataset with id {missing_id} not found in scene graph."):
+            pipeline_instance.remove_dataset(missing_id)
+
+        mock_logger.error.assert_called_once_with(f"Dataset with id {missing_id} not found in scene graph.")
+        pipeline_instance._scene_graph.remove_dataset.assert_not_called()
+
+def test_remove_dataset_calls_renderer_remove_actor():
+    """remove_dataset deregisters each subtree leaf via the renderer."""
+    instance = make_fully_mocked_visorscene()
+    dataset_id = 42
+    leaf_node = MagicMock(name="leaf_node")
+    leaf_node.id = 555
+    subtree = MagicMock(name="scene_subtree")
+    subtree.get_descendant_part_nodes.return_value = [leaf_node]
+
+    instance._scene_graph.get_descendant_node.return_value = subtree
+    instance._scene_graph.remove_dataset = MagicMock()
+    instance._dataset_registry.datasets = {dataset_id: MagicMock()}
+
+    instance.remove_dataset(dataset_id)
+
+    # remove_dataset iterates the subtree's leaves and delegates each to
+    # renderer.deregister_node.  The renderer owns the pipeline registry;
+    # coordinator no longer holds a copy.
+    instance._renderer.deregister_node.assert_called_once_with(leaf_node.id)
+    instance._scene_graph.remove_dataset.assert_called_once_with(dataset_id)
+    assert dataset_id not in instance._dataset_registry.datasets
+
+
+
+def test_dataset_count_delegates(pipeline_instance):
+    """Test that dataset_count delegates."""
+    pipeline_instance._dataset_registry.count = 5
+    assert pipeline_instance.dataset_count == 5
+
+
+def test_list_dataset_state_info_delegates(pipeline_instance):
+    """Test that list_all_dataset_info delegates."""
+    expected = {123: {"name": "foo"}}
+    pipeline_instance._dataset_registry.list_info.return_value = expected
+    assert pipeline_instance.list_all_dataset_info() == expected
+    pipeline_instance._dataset_registry.list_info.assert_called_once()
+
+
+def test_get_scene_details_returns_model(pipeline_instance):
+    """Test that get_scene_details returns model."""
+    pipeline_instance._dataset_registry.unit = "m"
+    pipeline_instance._dataset_registry.dictionaries = {}
+    pipeline_instance._dataset_registry.runtime_state_dict = {}
+
+    details = pipeline_instance.get_scene_details()
+
+    assert details.app_state.ui.dark_theme == pipeline_instance.dark_mode
+    assert details.app_state.scene.unit == "m"
+    assert details.app_state.scene.dataset_states == {}
+    assert details.vtk_info.renderer_annotation.widgets.orientation_widget_id == 10
+    assert details.vtk_info.renderer_annotation.widgets.cross_section_plane_id == 20
+    assert details.vtk_info.renderer_annotation.widgets.cross_section_plane_widget_id == 21
+    assert (
+        details.vtk_info.renderer_annotation.widgets.cross_section_plane_representation_id == 22
+    )
+    assert details.vtk_info.renderer_annotation.widgets.bounding_box_algorithm_id == 30
+    assert details.vtk_info.renderer_annotation.widgets.bounding_box_outline_actor_id == 31
+    assert details.vtk_info.renderer_annotation.widgets.bounding_box_axes_actor_id == 32
+
+
+def test_get_scene_details_json_is_valid(pipeline_instance):
+    """Test that get_scene_details_json is valid."""
+    # Ensure unit and state are simple, serializable values
+    pipeline_instance._dataset_registry.unit = "m"
+    pipeline_instance._dataset_registry.dictionaries = {}
+    pipeline_instance._dataset_registry.runtime_state_dict = {}
+
+
+    payload = json.loads(pipeline_instance.get_scene_details_json())
+
+    # Top-level fields
+    assert payload["appState"]["scene"]["unit"] == "m"
+    assert payload["appState"]["ui"]["darkTheme"] == pipeline_instance.dark_mode
+
+    # Widget IDs are ints, now under rendererAnnotation.widgets
+    widgets = payload["vtkInfo"]["rendererAnnotation"]["widgets"]
+    assert widgets["orientationWidgetId"] == 10
+    assert widgets["crossSectionPlaneId"] == 20
+    assert widgets["crossSectionPlaneWidgetId"] == 21
+    assert widgets["crossSectionPlaneRepresentationId"] == 22
+    assert widgets["boundingBoxAlgorithmId"] == 30
+    assert widgets["boundingBoxOutlineActorId"] == 31
+    assert widgets["boundingBoxAxesActorId"] == 32
+
+    # Scene graph and state presence
+    assert "sceneGraph" in payload["vtkInfo"]
+    dataset_dictionaries = payload["appState"]["scene"]["datasetStates"]
+    assert dataset_dictionaries == {}
+
+
+def test_cleanup_state_removes_keys(pipeline_instance):
+    """Test that cleanup_state removes keys."""
+    pipeline_instance._server.state["wasm_ids"] = {"x": 1}
+    pipeline_instance._server.state["wasm_ref_name"] = {"y": 2}
+
+    pipeline_instance.cleanup_state()
+
+    assert "wasm_ids" not in pipeline_instance._server.state
+    assert "wasm_ref_name" not in pipeline_instance._server.state
+
+
+def test_clear_resets_scene_and_delegates_to_registry(pipeline_instance):
+    """clear() delegates to the renderer's deregister_all() and resets scene."""
+    pipeline_instance.clear()
+
+    assert pipeline_instance._dataset_registry.clear.call_count == 1
+    pipeline_instance._renderer.deregister_all.assert_called_once()
+    assert pipeline_instance._scene_graph is None
+
+
+
+def test_populate_scene_calls_update_widgets(pipeline_instance):
+    """Test that populate_scene() calls update_widgets."""
+    # No part nodes in graph, just ensure update_widgets is called.
+    pipeline_instance._scene_graph.get_descendant_part_nodes.return_value = []
+    spy = MagicMock()
+    pipeline_instance.update_widgets = spy
+
+    pipeline_instance.populate_scene()
+
+    spy.assert_called_once()
+
+
+def test_update_widgets_calls_internal_helpers(pipeline_instance):
+    """Test that update_widgets calls internal helpers."""
+    pipeline_instance._update_widget_bounds = MagicMock()
+    pipeline_instance._update_actor_count = MagicMock()
+
+    pipeline_instance.update_widgets()
+
+    pipeline_instance._update_widget_bounds.assert_called_once()
+    pipeline_instance._update_actor_count.assert_called_once()
+
+
+def test_update_widgets_raises_without_scene_graph(pipeline_instance):
+    """Test that update_widgets raises without scene graph."""
+    pipeline_instance._scene_graph = None
+    with pytest.raises(RuntimeError):
+        pipeline_instance.update_widgets()
+
+
+def test_add_dataset_delegates_and_builds_state(pipeline_instance):
+    """Test that add_dataset delegates and builds state."""
+    dataset = dummy_dataset()
+    metadata = SimpleNamespace(
+        name="test_model",
+        unit="m",
+        file_path=None,
+        metadata_path=None,
+        state=SimpleNamespace(partProperties={}),
+    )
+
+    pipeline_instance.add_dataset(dataset, metadata)
+
+    # Assert on public behavior: add() was called with correct arguments
+    pipeline_instance._dataset_registry.add.assert_called_once()
+    args, _ = pipeline_instance._dataset_registry.add.call_args
+    ds_id, ds_name, ds_input, ds_part_name_to_id, ds_metadata = args
+
+    assert ds_id == 123  # from scene_graph.load_dataset mock
+    assert ds_name == "test_model"
+    assert ds_input is dataset
+    assert ds_part_name_to_id == {}  # from scene_graph.get_part_name_to_id_map mock
+    assert ds_metadata is metadata
+
+
+def test_remove_dataset_success_path(pipeline_instance):
+    """remove_dataset deregisters each subtree leaf via the renderer."""
+    # Configure the dataset-node subtree to expose two leaves.  The renderer
+    # owns the pipeline registry post-I5, so the coordinator does not seed it.
+    leaf1 = MagicMock()
+    leaf1.id = 501
+    leaf2 = MagicMock()
+    leaf2.id = 502
+    dataset_node = pipeline_instance._scene_graph.get_descendant_node(123)
+    dataset_node.get_descendant_part_nodes.return_value = [leaf1, leaf2]
+
+    pipeline_instance.remove_dataset(123)
+
+    assert pipeline_instance._renderer.deregister_node.call_count == 2
+    pipeline_instance._renderer.deregister_node.assert_any_call(leaf1.id)
+    pipeline_instance._renderer.deregister_node.assert_any_call(leaf2.id)
+
+    pipeline_instance._scene_graph.remove_dataset.assert_called_once_with(123)
+    pipeline_instance._dataset_registry.remove.assert_called_once_with(123)
+
+
+def test_remove_dataset_raises_when_not_found(pipeline_instance):
+    """Test that remove_dataset raises when not found."""
+    with pytest.raises(ValueError):
+        pipeline_instance.remove_dataset(999)
+    pipeline_instance._dataset_registry.remove.assert_not_called()
+
+
+def test_list_variables_for_dataset_delegates(pipeline_instance):
+    """Test that list_variables_for_dataset delegates."""
+    expected = [MagicMock()]
+    pipeline_instance._dataset_registry.list_variables.return_value = expected
+    result = pipeline_instance.list_variables_for_dataset(123)
+    assert result == expected
+    pipeline_instance._dataset_registry.list_variables.assert_called_once_with(123)
+
+
+def test_update_variables_for_dataset_triggers_updates_and_render(pipeline_instance):
+    """Test that update_variables_for_dataset triggers updates and render."""
+    vars_payload = ["dummy"]
+    render_spy = MagicMock()
+    pipeline_instance.render = render_spy
+
+    pipeline_instance.update_variables_for_dataset(123, vars_payload)
+
+    pipeline_instance._dataset_registry.update_variables.assert_called_once_with(123, vars_payload)
+    dataset_node = pipeline_instance._scene_graph.get_descendant_node(123)
+    dataset_node.refresh_descendant_variable_metadata.assert_called_once_with(include_self=True)
+    render_spy.assert_called_once()
+
+
+def test_render_triggers_window_and_view(pipeline_instance):
+    """Test that render() delegates to the renderer backend."""
+    pipeline_instance.render()
+    pipeline_instance._renderer.render.assert_called_once()
+
+
+def test_reset_camera_with_scene_graph(pipeline_instance):
+    """Test reset_camera() delegates to the renderer with scene-graph bounds."""
+    pipeline_instance.reset_camera()
+    pipeline_instance._renderer.reset_camera.assert_called_once_with(pipeline_instance._scene_graph.bounds)
+
+
+def test_reset_camera_no_scene_graph_noop(pipeline_instance):
+    """Test that reset_camera when no scene graph results in no-op."""
+    pipeline_instance._renderer.reset_camera.reset_mock()
+    pipeline_instance._scene_graph = None
+
+    pipeline_instance.reset_camera()
+
+    pipeline_instance._renderer.reset_camera.assert_not_called()
+
+
+# ---------------------------------------------------------------------------
+# Tests moved from tests/unit/app/test_finalize_scene.py
+# ---------------------------------------------------------------------------
+
+
+from ansys.visor.viewer.vtk.scene.base import VisorSceneBase
+
+
+class DummyScene(VisorSceneBase):
+    """A minimal subclass of VisorSceneBase that avoids full VTK initialization.
+
+    We override heavy initialization and supply only the attributes used by
+    `finalize_scene` so tests can run without VTK/trame.
+    """
+
+    def __init__(self):
+        # do not call super().__init__ to avoid VTK setup
+        # set only the attributes used by finalize_scene
+        self._dataset_registry = SimpleNamespace(count=0)
+        self._populate_called = False
+        self._reset_called = False
+        self._render_called = False
+
+    # Implement the two abstract hooks with no-op stubs so the class is
+    # instantiable.  These stubs are never called by finalize_scene tests.
+    async def _get_runtime_state_async(self, timeout: float):
+        return None
+
+    def _apply_runtime_state_to_render(self, runtime_app_state) -> None:
+        pass
+
+    def populate_scene(self):
+        self._populate_called = True
+
+    def reset_camera(self):
+        self._reset_called = True
+
+    def render(self):
+        self._render_called = True
+
+
+def test_finalize_scene_resets_camera_when_one_dataset():
+    """Test that finalize_scene resets camera when one dataset is loaded in scene."""
+    s = DummyScene()
+    s._dataset_registry.count = 1
+
+    s.finalize_scene()
+
+    assert s._populate_called is True
+    assert s._reset_called is True
+    assert s._render_called is True
+
+
+def test_finalize_scene_skips_reset_when_flag_true():
+    """Test that finalize_scene skips reset() when flag is true."""
+    s = DummyScene()
+    s._dataset_registry.count = 1
+
+    s.finalize_scene(skip_reset_camera=True)
+
+    assert s._populate_called is True
+    assert s._reset_called is False
+    assert s._render_called is True
+
+
+def test_finalize_scene_no_reset_when_multiple_datasets():
+    """Test that finalize_scene does not reset camera when multiple datasets."""
+    s = DummyScene()
+    s._dataset_registry.count = 2
+
+    s.finalize_scene()
+
+    assert s._populate_called is True
+    # with more than one dataset, camera should not be reset
+    assert s._reset_called is False
+    assert s._render_called is True
+
diff --git a/tests/unit/vtk/scene/test_scene_graph_state_builder.py b/tests/unit/vtk/scene/test_scene_graph_state_builder.py
new file mode 100644
index 00000000..e381c0bd
--- /dev/null
+++ b/tests/unit/vtk/scene/test_scene_graph_state_builder.py
@@ -0,0 +1,77 @@
+"""Unit tests for SceneGraphStateBuilder."""
+
+from unittest.mock import MagicMock
+
+from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo
+from ansys.visor.viewer.vtk.scene.scene_graph_state_builder import SceneGraphStateBuilder
+
+
+def _leaf_info(node_id: int, name: str = "p") -> SceneGraphNodeInfo:
+    return SceneGraphNodeInfo(
+        id=node_id, name=name, is_actor_node=True, is_group_node=False,
+        node_type="vtkPolyData",
+    )
+
+
+def _root_info() -> SceneGraphNodeInfo:
+    return SceneGraphNodeInfo(
+        id=1, name="root", is_actor_node=False, is_group_node=True, node_type="root",
+    )
+
+
+# ---------------------------------------------------------------------------
+# Structure only: the builder reads the scene graph and recurses; it carries
+# no renderer handles.
+# ---------------------------------------------------------------------------
+
+def test_builder_recurses_through_group_nodes():
+    root_info = _root_info()
+    group_info = SceneGraphNodeInfo(
+        id=2, name="grp", is_actor_node=False, is_group_node=True, node_type="vtkMultiBlockDataSet",
+    )
+    a_info = _leaf_info(101, "a")
+    b_info = _leaf_info(102, "b")
+
+    a = MagicMock()
+    a.id = 101
+    a.is_part_node = True
+    a.is_group_node = False
+    a.state = a_info
+    b = MagicMock()
+    b.id = 102
+    b.is_part_node = True
+    b.is_group_node = False
+    b.state = b_info
+    group = MagicMock()
+    group.is_part_node = False
+    group.is_group_node = True
+    group.state = group_info
+    group.children = [a, b]
+    root = MagicMock()
+    root.is_part_node = False
+    root.is_group_node = True
+    root.state = root_info
+    root.children = [group]
+
+    out = SceneGraphStateBuilder(root).build()
+
+    assert out is root_info
+    (nested_group,) = out.children
+    assert nested_group is group_info
+    assert [c.name for c in nested_group.children] == ["a", "b"]
+
+
+def test_builder_handles_empty_root():
+    root_info = _root_info()
+    root = MagicMock()
+    root.is_part_node = False
+    root.is_group_node = True
+    root.state = root_info
+    root.children = []
+
+    out = SceneGraphStateBuilder(root).build()
+
+    assert out is root_info
+    assert out.children == []
+
+
diff --git a/tests/unit/vtk/scene/test_visor_frontend_bridge.py b/tests/unit/vtk/scene/test_visor_frontend_bridge.py
new file mode 100644
index 00000000..fd5f8a79
--- /dev/null
+++ b/tests/unit/vtk/scene/test_visor_frontend_bridge.py
@@ -0,0 +1,314 @@
+import asyncio
+
+import pytest
+
+from ansys.visor.viewer.vtk.scene.visor_frontend_bridge import VisorFrontendBridge
+
+# ------------------------------------------------------------------
+# Helpers
+# ------------------------------------------------------------------
+
+class FakeServer:
+    """ A fake server that records JS calls made to it."""
+    def __init__(self):
+        self.calls = []
+
+    def js_call(self, ref, method, payload):
+        self.calls.append((ref, method, payload))
+
+
+
+# ------------------------------------------------------------------
+# set_state
+# ------------------------------------------------------------------
+
+def test_set_state_sends_js_call(monkeypatch):
+    """set_state should send a JS call with serialized request."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.get_random_javascript_safe_id",
+        lambda: 123,
+    )
+
+    class FakeRequest:
+        def __init__(self, request_id, app_state): # noqa: N803
+            self.request_id = request_id
+            self.app_state = app_state
+
+        def model_dump(self, by_alias=True):
+            return {"id": self.request_id}
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorLoadStateRequest",
+        FakeRequest,
+    )
+
+    server = FakeServer()
+
+    bridge = VisorFrontendBridge(server, "visor-frontend-ref")
+    result = bridge.set_state("state")
+
+    assert result == 123
+    assert bridge._ref_name == "visor-frontend-ref"
+    assert server.calls[0][0] == "visor-frontend-ref"
+    assert server.calls[0][1] == "setState"
+    assert server.calls[0][2] == {"id": 123}
+
+
+# ------------------------------------------------------------------
+# request_state
+# ------------------------------------------------------------------
+
+@pytest.mark.asyncio
+async def test_request_state_resolves(monkeypatch):
+    """request_state should await and return resolved response."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.get_random_javascript_safe_id",
+        lambda: 10,
+    )
+
+    class FakeRequest:
+        def __init__(self, request_id): # noqa: N803
+            self.request_id = request_id
+
+        def model_dump(self, exclude_none=True, by_alias=True):
+            return {"requestId": self.request_id}
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateRequest",
+        FakeRequest,
+    )
+
+    # โœ… FIX: mock model_validate
+    class FakeResponse:
+        def __init__(self, request_id): # noqa: N803
+            self.request_id = request_id
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateResponse.model_validate",
+        lambda data: FakeResponse(data["requestId"]),
+    )
+
+    server = FakeServer()
+    bridge = VisorFrontendBridge(server, "visor-frontend-ref")
+
+    async def resolve_later():
+        await asyncio.sleep(0)
+        bridge.resolve_save_state_response(10, {"requestId": 10})
+
+    asyncio.create_task(resolve_later())
+
+    response = await bridge.request_state(timeout=1)
+
+    assert response.request_id == 10
+    assert len(server.calls) == 1
+    assert bridge._ref_name == "visor-frontend-ref"
+    assert server.calls[0][0] == "visor-frontend-ref"
+    assert server.calls[0][1] == "getState"
+
+
+@pytest.mark.asyncio
+async def test_request_state_cleans_up(monkeypatch):
+    """Pending request should be removed after completion."""
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.get_random_javascript_safe_id",
+        lambda: 1,
+    )
+
+    class FakeRequest:
+        def __init__(self, request_id): # noqa: N803
+            self.request_id = request_id
+
+        def model_dump(self, exclude_none=True, by_alias=True):
+            return {"requestId": self.request_id}
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateRequest",
+        FakeRequest,
+    )
+
+    # โœ… FIX: mock model_validate
+    class FakeResponse:
+        def __init__(self, request_id): # noqa: N803
+            self.request_id = request_id
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateResponse.model_validate",
+        lambda data: FakeResponse(data["requestId"]),
+    )
+
+    server = FakeServer()
+    bridge = VisorFrontendBridge(server, "visor-frontend-ref")
+
+    async def resolve():
+        await asyncio.sleep(0)
+        bridge.resolve_save_state_response(1, {"requestId": 1})
+
+    asyncio.create_task(resolve())
+    await bridge.request_state(timeout=1)
+
+    assert bridge._pending_requests == {}
+    assert bridge._pending_loop is None
+
+
+# ------------------------------------------------------------------
+# resolve_save_state_response
+# ------------------------------------------------------------------
+
+def test_resolve_sets_result(monkeypatch):
+    """resolve_save_state_response should complete pending future."""
+
+    class FakeResponse:
+        def __init__(self, request_id): # noqa: N803
+            self.request_id = request_id
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateResponse.model_validate",
+        lambda data: FakeResponse(data["requestId"]),
+    )
+
+    bridge = VisorFrontendBridge(FakeServer(), "visor-frontend-ref")
+
+    loop = asyncio.new_event_loop()
+    fut = loop.create_future()
+
+    bridge._pending_requests[5] = fut
+    bridge._pending_loop = None
+
+    bridge.resolve_save_state_response(5, {"requestId": 5})
+
+    assert fut.done()
+    assert fut.result().request_id == 5
+
+
+def test_resolve_invalid_response_sets_exception(monkeypatch):
+    """Invalid response should set exception on future."""
+
+    from pydantic import ValidationError
+
+    def raise_error(_):
+        raise ValidationError.from_exception_data("x", [])
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateResponse.model_validate",
+        raise_error,
+    )
+
+    bridge = VisorFrontendBridge(FakeServer(), "visor-frontend-ref")
+
+    loop = asyncio.new_event_loop()
+    fut = loop.create_future()
+
+    bridge._pending_requests[1] = fut
+    bridge._pending_loop = None
+
+    with pytest.raises(ValueError):
+        bridge.resolve_save_state_response(1, {})
+
+    assert fut.done()
+    assert isinstance(fut.exception(), ValidationError)
+
+
+def test_resolve_unknown_request_logs_warning(monkeypatch):
+    """Unknown request_id should trigger warning."""
+
+    warnings = []
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.logger.warning",
+        lambda msg: warnings.append(msg),
+    )
+
+    class FakeResponse:
+        def __init__(self, request_id): # noqa: N803
+            self.request_id = request_id
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateResponse.model_validate",
+        lambda data: FakeResponse(data["requestId"]),
+    )
+
+    bridge = VisorFrontendBridge(FakeServer(), "visor-frontend-ref")
+
+    bridge.resolve_save_state_response(999, {"requestId": 999})
+
+    assert len(warnings) == 1
+    assert "no pending async waiter" in warnings[0]
+
+
+# ------------------------------------------------------------------
+# threadsafe helpers
+# ------------------------------------------------------------------
+
+def test_threadsafe_set_result_no_loop():
+    """_threadsafe_set_result should set result without loop."""
+
+    bridge = VisorFrontendBridge(None, "")
+
+    loop = asyncio.new_event_loop()
+    fut = loop.create_future()
+
+    bridge._threadsafe_set_result(None, fut, "value")
+
+    assert fut.done()
+    assert fut.result() == "value"
+
+
+def test_threadsafe_set_exception_no_loop():
+    """_threadsafe_set_exception should set exception without loop."""
+
+    bridge = VisorFrontendBridge(None, "")
+
+    loop = asyncio.new_event_loop()
+    fut = loop.create_future()
+
+    exc = RuntimeError("error")
+    bridge._threadsafe_set_exception(None, fut, exc)
+
+    assert fut.done()
+    assert fut.exception() is exc
+
+
+def test_threadsafe_helpers_ignore_done_future():
+    """Threadsafe helpers should not overwrite completed futures."""
+
+    bridge = VisorFrontendBridge(None, "")
+
+    loop = asyncio.new_event_loop()
+    fut = loop.create_future()
+    fut.set_result("done")
+
+    bridge._threadsafe_set_result(None, fut, "new")
+    bridge._threadsafe_set_exception(None, fut, RuntimeError())
+
+    assert fut.result() == "done"
+
+def test_resolve_unexpected_exception_sets_exception_and_reraises(monkeypatch):
+    """Unexpected exception during validation should set future exception and re-raise."""
+
+    # Force a non-ValidationError exception
+    def raise_unexpected(_):
+        raise RuntimeError("boom")
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_frontend_bridge.VisorSaveStateResponse.model_validate",
+        raise_unexpected,
+    )
+
+    bridge = VisorFrontendBridge(FakeServer(), "visor-frontend-ref")
+
+    loop = asyncio.new_event_loop()
+    fut = loop.create_future()
+
+    bridge._pending_requests[42] = fut
+    bridge._pending_loop = None
+
+    with pytest.raises(RuntimeError):
+        bridge.resolve_save_state_response(42, {"requestId": 42})
+
+    # Future should be completed with the same exception
+    assert fut.done()
+    assert isinstance(fut.exception(), RuntimeError)
+    assert str(fut.exception()) == "boom"
diff --git a/tests/unit/vtk/scene/test_visor_state_mapper.py b/tests/unit/vtk/scene/test_visor_state_mapper.py
new file mode 100644
index 00000000..89a07437
--- /dev/null
+++ b/tests/unit/vtk/scene/test_visor_state_mapper.py
@@ -0,0 +1,245 @@
+import pytest
+
+from ansys.visor.viewer.vtk.scene.visor_state_mapper import VisorStateMapper
+
+# ------------------------------------------------------------------
+# Helpers / fakes
+# ------------------------------------------------------------------
+
+class FakeDataset:
+    """Fake dataset for testing."""
+    def __init__(self, name, did):
+        self.name = name
+        self.id = did
+
+    def runtime_to_persisted_state(self, state):
+        return {"converted": state}
+
+    def persisted_to_runtime_state(self, parts):
+        return {"rt": parts}
+
+
+class FakeRegistry:
+    """ Fake dataset registry for testing."""
+    def __init__(self, datasets=None, by_name=None):
+        self.datasets = datasets or {}
+        self._by_name = by_name or {}
+
+    def get_by_name(self, name):
+        return self._by_name.get(name)
+
+
+# ------------------------------------------------------------------
+# Tests: init
+# ------------------------------------------------------------------
+
+def test_init_requires_registry():
+    """Constructor should require dataset registry."""
+    with pytest.raises(ValueError):
+        VisorStateMapper(None)
+
+
+# ------------------------------------------------------------------
+# runtime_to_persisted
+# ------------------------------------------------------------------
+
+def test_runtime_to_persisted_basic(monkeypatch):
+    """runtime_to_persisted should convert datasets and pass through fields."""
+
+    dataset = FakeDataset(name="dsA", did=1)
+    registry = FakeRegistry(datasets={1: dataset})
+
+    called = {}
+
+    def fake_from_components(**kwargs):
+        called.update(kwargs)
+        return {"result": "ok"}
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_state_mapper.PersistedViewerStateV1.from_components",
+        fake_from_components,
+    )
+
+    runtime_app_state = type("RuntimeAppState", (), {
+        "ui": type("UI", (), {})(),
+        "scene": type("Scene", (), {
+            "unit": "m",
+            "camera": "cam",
+            "spectrum_states": {"var": 1},
+            "dataset_states": {1: {"state": 123}},
+            "cross_section": "cs",
+            "orthographic_enabled": True,
+            "cross_section_enabled": False,
+            "edges_enabled": True,
+            "bounding_box_enabled": False,
+        })(),
+    })()
+
+    mapper = VisorStateMapper(registry)
+    result = mapper.runtime_to_persisted(runtime_app_state)
+
+    assert result == {"result": "ok"}
+    assert called["datasets"] == {"dsA": {"converted": {"state": 123}}}
+    assert called["unit"] == "m"
+    assert called["camera"] == "cam"
+    assert called["variable_states"] == {"var": 1}
+
+
+def test_runtime_to_persisted_skips_missing_dataset(monkeypatch):
+    """Datasets missing from registry should be skipped."""
+
+    registry = FakeRegistry(datasets={})
+
+    captured = {}
+
+    def fake_from_components(**kwargs):
+        captured.update(kwargs)
+        return "out"
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_state_mapper.PersistedViewerStateV1.from_components",
+        fake_from_components,
+    )
+
+    runtime_app_state = type("RuntimeAppState", (), {
+        "ui": object(),
+        "scene": type("Scene", (), {
+            "unit": "m",
+            "camera": "cam",
+            "spectrum_states": {},
+            "dataset_states": {123: {"data": 1}},
+            "cross_section": None,
+            "orthographic_enabled": False,
+            "cross_section_enabled": False,
+            "edges_enabled": False,
+            "bounding_box_enabled": False,
+        })(),
+    })()
+
+    mapper = VisorStateMapper(registry)
+    mapper.runtime_to_persisted(runtime_app_state)
+
+    assert captured["datasets"] == {}
+
+
+# ------------------------------------------------------------------
+# persisted_to_runtime
+# ------------------------------------------------------------------
+
+def test_persisted_to_runtime_basic(monkeypatch):
+    """persisted_to_runtime should convert dataset states via registry."""
+
+    dataset = FakeDataset(name="dsA", did=10)
+    registry = FakeRegistry(by_name={"dsA": dataset})
+
+    captured = {}
+
+    def fake_from_components(**kwargs):
+        captured.update(kwargs)
+        return {"ok": True}
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_state_mapper.RuntimeAppState.from_components",
+        fake_from_components,
+    )
+
+    state = type("Persisted", (), {
+        "ui": type("UI", (), {"dark_theme": True})(),
+        "scene": type("Scene", (), {
+            "unit": "mm",
+            "camera": "cam",
+            "variable_states": {"v": 1},
+            "dataset_states": {
+                "dsA": type("DS", (), {"parts": {"p": 9}})()
+            },
+            "cross_section": "cs",
+            "orthographic_enabled": True,
+            "cross_section_enabled": False,
+            "edges_enabled": True,
+            "bounding_box_enabled": False,
+        })(),
+    })()
+
+    mapper = VisorStateMapper(registry)
+    result = mapper.persisted_to_runtime(state)
+
+    assert result == {"ok": True}
+    assert captured["dataset_states"] == {10: {"rt": {"p": 9}}}
+    assert captured["unit"] == "mm"
+    assert captured["camera"] == "cam"
+
+
+def test_persisted_to_runtime_missing_dataset_logs_warning(monkeypatch):
+    """Missing dataset in registry should log warning and skip."""
+
+    registry = FakeRegistry(by_name={})
+
+    warnings = []
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_state_mapper.logger.warning",
+        lambda msg: warnings.append(msg),
+    )
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_state_mapper.RuntimeAppState.from_components",
+        lambda **kwargs: kwargs,
+    )
+
+    state = type("Persisted", (), {
+        "ui": type("UI", (), {"dark_theme": False})(),
+        "scene": type("Scene", (), {
+            "unit": None,
+            "camera": None,
+            "variable_states": {},
+            "dataset_states": {
+                "missing": type("DS", (), {"parts": {}})()
+            },
+            "cross_section": None,
+            "orthographic_enabled": False,
+            "cross_section_enabled": False,
+            "edges_enabled": False,
+            "bounding_box_enabled": False,
+        })(),
+    })()
+
+    mapper = VisorStateMapper(registry)
+    result = mapper.persisted_to_runtime(state)
+
+    assert result["dataset_states"] == {}
+    assert len(warnings) == 1
+    assert "not found in registry" in warnings[0]
+
+
+def test_persisted_to_runtime_handles_none_variable_states(monkeypatch):
+    """None variable_states should default to empty dict."""
+
+    dataset = FakeDataset(name="dsA", did=1)
+    registry = FakeRegistry(by_name={"dsA": dataset})
+
+    captured = {}
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene.visor_state_mapper.RuntimeAppState.from_components",
+        lambda **kwargs: captured.update(kwargs) or kwargs,
+    )
+
+    state = type("Persisted", (), {
+        "ui": type("UI", (), {"dark_theme": False})(),
+        "scene": type("Scene", (), {
+            "unit": "m",
+            "camera": "cam",
+            "variable_states": None,
+            "dataset_states": {},
+            "cross_section": None,
+            "orthographic_enabled": False,
+            "cross_section_enabled": False,
+            "edges_enabled": False,
+            "bounding_box_enabled": False,
+        })(),
+    })()
+
+    mapper = VisorStateMapper(registry)
+    mapper.persisted_to_runtime(state)
+
+    assert captured["variable_states"] == {}
diff --git a/tests/unit/vtk/scene_graph/test_base_node.py b/tests/unit/vtk/scene_graph/test_base_node.py
new file mode 100644
index 00000000..f24600da
--- /dev/null
+++ b/tests/unit/vtk/scene_graph/test_base_node.py
@@ -0,0 +1,414 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from ansys.visor.viewer.vtk.scene_graph.base_node import NodeCache, VisorSceneGraphNode
+
+
+# Concrete subclass for testing abstract base
+class DummyNode(VisorSceneGraphNode):
+    """A dummy subclass of VisorSceneGraphNode for testing purposes."""
+    def _post_init(self, parent, node_metadata, dataset, root_node):
+        self._node_type = MagicMock()
+        self._vtk_dataset_type = "DummyType"
+        self._bounds = [0, 1, 2, 3, 4, 5]
+        self._children = []
+        if node_metadata:
+            self._set_name(node_metadata)
+        else:
+            self._name = "dummy"
+
+def test_nodecache_basic_set_get_clear():
+    """Verify that NodeCache supports storing, retrieving, and clearing values."""
+    cache = NodeCache()
+    key = cache.make_key("kind", True, None)
+    assert cache.get(key) is None
+    cache.set(key, "value")
+    assert cache.get(key) == "value"
+    cache.clear()
+    assert cache.get(key) is None
+
+def test_nodecache_filter_func_cache_key():
+    """Verify that cache keys can be generated from filter functions."""
+    cache = NodeCache()
+    def f(): pass
+    key1 = cache._filter_func_cache_key(None)
+    key2 = cache._filter_func_cache_key(f)
+    key3 = cache._filter_func_cache_key(lambda x: x)
+    assert key1 is None
+    assert isinstance(key2, tuple) or isinstance(key2, list)
+    assert isinstance(key3, tuple) or isinstance(key3, list)
+
+def test_dummy_node_properties():
+    """Verify that node properties and state expose the expected values."""
+    node = DummyNode(None)
+    assert node.id == node._id
+    assert node.name == "dummy"
+    assert node.vtk_dataset_type == "DummyType"
+    # _node_type is a MagicMock, so is_part_node/is_group_node are False
+    assert node.is_part_node is False
+    assert node.is_group_node is False
+    assert node.bounds == [0, 1, 2, 3, 4, 5]
+    state = node.state
+    assert state.id == node.id
+    assert state.name == node.name
+
+def test_set_name_with_metadata():
+    """Verify that the node name is initialized from metadata."""
+    node_metadata = MagicMock()
+    node_metadata.Has.return_value = True
+    node_metadata.Get.return_value = "meta_name"
+    node = DummyNode(None, node_metadata=node_metadata)
+    assert node.name == "meta_name"
+
+def test_set_name_no_metadata_raises():
+    """Verify that _set_name raises when metadata is not provided."""
+    node = DummyNode(None)
+    with pytest.raises(RuntimeError, match="node_metadata should not be null here"):
+        node._set_name(None)
+
+def test_set_name_empty_name_defaults():
+    """Verify that unnamed metadata results in the default node name."""
+    node_metadata = MagicMock()
+    node_metadata.Has.return_value = False
+    node_metadata.Get.return_value = ""
+    node = DummyNode(None, node_metadata=node_metadata)
+    assert node.name == "untitled"
+
+def test_get_descendant_nodes_and_dict(monkeypatch):
+    """Verify that descendant-node queries return the expected nodes."""
+    node = DummyNode(None)
+    child = DummyNode(node)
+    node._children = [child]
+    # Should include child only
+    nodes = node.get_descendant_nodes()
+    assert child in nodes
+    # Should include self and child
+    nodes2 = node.get_descendant_nodes(include_self=True)
+    assert node in nodes2 and child in nodes2
+    # Should filter by a function
+    nodes3 = node.get_descendant_nodes(filter_func=lambda n: n.name == "dummy")
+    assert all(n.name == "dummy" for n in nodes3)
+
+def test_get_descendant_node_by_id():
+    """Verify that descendant nodes can be located by ID."""
+    node = DummyNode(None)
+    child = DummyNode(node)
+    node._children = [child]
+    found = node.get_descendant_node(child.id)
+    assert found is child
+    not_found = node.get_descendant_node(99999)
+    assert not_found is None
+
+def test_remove_node_group(monkeypatch):
+    """Verify that child nodes can be removed from group nodes."""
+    node = DummyNode(None)
+    node._node_type = MagicMock(return_value=True)
+    node._children = [DummyNode(node)]
+    # Patch is_group_node to True
+    monkeypatch.setattr(DummyNode, "is_group_node", property(lambda self: True))
+    child_id = node._children[0].id
+    assert node.remove_node(child_id) is True
+    assert node._children == []
+
+def test_remove_node_actor(monkeypatch):
+    """Verify that remove_node returns False for missing actor nodes."""
+    node = DummyNode(None)
+    node._node_type = MagicMock(return_value=False)
+    # Patch is_group_node to False
+    monkeypatch.setattr(DummyNode, "is_group_node", property(lambda self: False))
+    assert node.remove_node(12345) is False
+
+def test_cleanup_descendants(monkeypatch):
+    """Verify that descendant cleanup removes child nodes."""
+    node = DummyNode(None)
+    child = DummyNode(node)
+    node._children = [child]
+    monkeypatch.setattr(DummyNode, "is_group_node", property(lambda self: True))
+    node._cleanup_descendants()
+    assert node._children == []
+
+def test_get_json():
+    """Verify that get_json returns a serialized representation of the node."""
+    node = DummyNode(None)
+    json_str = node.get_json()
+    import json
+    data = json.loads(json_str)
+    assert data["id"] == node.id
+
+def test_clear_cache_on_children():
+    """Verify that cache clearing is applied recursively to child nodes."""
+    node = DummyNode(None)
+    child = DummyNode(node)
+    node._children = [child]
+    node._cache.set(("a",), "x")
+    child._cache.set(("b",), "y")
+    node._clear_cache()
+    assert node._cache.get(("a",)) is None
+    assert child._cache.get(("b",)) is None
+
+def test_get_node_type_from_dataset_supported(monkeypatch):
+    """Verify that supported VTK datasets map to a scene-graph node type."""
+    from vtkmodules import vtkCommonDataModel
+
+    class DummyMultiBlock(vtkCommonDataModel.vtkMultiBlockDataSet):
+        pass
+
+    class DummyUnstructured(vtkCommonDataModel.vtkUnstructuredGrid):
+        pass
+
+    monkeypatch.setattr(vtkCommonDataModel, "vtkMultiBlockDataSet", type("vtkMultiBlockDataSet", (), {}))
+    monkeypatch.setattr(vtkCommonDataModel, "vtkUnstructuredGrid", type("vtkUnstructuredGrid", (), {}))
+    assert VisorSceneGraphNode.get_node_type_from_dataset(DummyMultiBlock()) is not None
+    assert VisorSceneGraphNode.get_node_type_from_dataset(DummyUnstructured()) is not None
+
+def test_get_node_type_from_dataset_unsupported():
+    """Verify that unsupported datasets raise an error."""
+    with pytest.raises(RuntimeError, match="node type not yet supported"):
+        VisorSceneGraphNode.get_node_type_from_dataset(object())
+
+# ------------------------------------------------------------------
+# Part-node descendant helpers
+# ------------------------------------------------------------------
+
+def test_part_node_paths(monkeypatch):
+    """Verify that part-node descendant helper methods return the expected results."""
+
+    class PartNode(DummyNode):
+        def _post_init(self, *args, **kwargs):
+            super()._post_init(*args, **kwargs)
+            monkeypatch.setattr(type(self), "is_part_node", property(lambda _: True))
+
+    root = DummyNode(None)
+    part = PartNode(root)
+    root._children = [part]
+
+    assert root.get_descendant_part_node(part.id) is part
+
+    nodes = root.get_descendant_part_nodes()
+    assert part in nodes
+
+
+def test_refresh_descendant_variable_metadata(monkeypatch):
+    """Should call refresh_variable_metadata on part nodes."""
+
+    class PartNode(DummyNode):
+        def _post_init(self, *args, **kwargs):
+            super()._post_init(*args, **kwargs)
+            monkeypatch.setattr(type(self), "is_part_node", property(lambda _: True))
+            self.refresh_variable_metadata = MagicMock()
+
+    root = DummyNode(None)
+    part = PartNode(root)
+    root._children = [part]
+
+    root.refresh_descendant_variable_metadata()
+
+    part.refresh_variable_metadata.assert_called_once()
+
+
+def test_descendant_part_count(monkeypatch):
+    """Count part nodes correctly."""
+
+    class PartNode(DummyNode):
+        def _post_init(self, *args, **kwargs):
+            super()._post_init(*args, **kwargs)
+            monkeypatch.setattr(type(self), "is_part_node", property(lambda _: True))
+
+    root = DummyNode(None)
+    a = PartNode(root)
+    b = PartNode(root)
+    root._children = [a, b]
+
+    assert root.descendant_part_count() == 2
+
+
+# ------------------------------------------------------------------
+# Mapping + cache behavior
+# ------------------------------------------------------------------
+
+def test_name_to_id_map():
+    """Should map names to IDs, including self."""
+
+    root = DummyNode(None)
+
+    child1 = DummyNode(root)
+    child1._name = "A"
+
+    child2 = DummyNode(root)
+    child2._name = "B"
+
+    root._children = [child1, child2]
+
+    result = root.get_descendant_node_name_to_id_map()
+
+    assert result["A"] == child1.id
+    assert result["B"] == child2.id
+    assert result[root.name] == root.id
+
+
+def test_descendant_cache_reuse(monkeypatch):
+    """Cache should be used on repeated calls."""
+
+    root = DummyNode(None)
+    child = DummyNode(root)
+    root._children = [child]
+
+    # First call populates cache
+    root.get_descendant_nodes()
+
+    call_count = {"count": 0}
+    original = root._get_descendant_node_dict
+
+    def wrapped(*args, **kwargs):
+        call_count["count"] += 1
+        return original(*args, **kwargs)
+
+    monkeypatch.setattr(root, "_get_descendant_node_dict", wrapped)
+
+    root.get_descendant_nodes()
+
+    # Should not recurse repeatedly if cache works
+    assert call_count["count"] <= 1
+
+
+def test_clear_cache_recursive():
+    """Clearing cache should propagate to children."""
+
+    root = DummyNode(None)
+    child = DummyNode(root)
+    root._children = [child]
+
+    root._cache.set(("a",), 1)
+    child._cache.set(("b",), 2)
+
+    root._clear_cache()
+
+    assert root._cache.get(("a",)) is None
+    assert child._cache.get(("b",)) is None
+
+
+# ------------------------------------------------------------------
+# Factory success path
+# ------------------------------------------------------------------
+
+def test_get_node_factory_success(monkeypatch):
+    """Factory should return correct subclass."""
+
+    class FakePartNode:
+        def __init__(self, *args, **kwargs):
+            self.created = True
+
+    class FakeGroupNode:
+        def __init__(self, *args, **kwargs):
+            self.created = True
+
+    # Patch node type detection
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.base_node.VisorSceneGraphNode.get_node_type_from_dataset",
+        lambda x: "part",
+    )
+
+    # Patch enum mapping
+    fake_enum = MagicMock(PART="part", GROUP="group")
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.base_node.VisorNodeType",
+        fake_enum,
+    )
+
+    # Patch actual classes imported inside function
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.VisorSceneGraphPartNode",
+        FakePartNode,
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.VisorSceneGraphGroupNode",
+        FakeGroupNode,
+    )
+
+    node = VisorSceneGraphNode.get_node(None)
+
+    assert isinstance(node, FakePartNode)
+
+def test_remove_node_recursive(monkeypatch):
+    """Should remove descendant node through recursive branch."""
+
+    # Force group behavior
+    monkeypatch.setattr(DummyNode, "is_group_node", property(lambda _: True))
+
+    root = DummyNode(None)
+    child = DummyNode(root)
+    grandchild = DummyNode(child)
+
+    root._children = [child]
+    child._children = [grandchild]
+
+    # Spy on cache clearing
+    root._clear_cache = MagicMock()
+    child._clear_cache = MagicMock()
+
+    result = root.remove_node(grandchild.id)
+
+    assert result is True
+
+    # Grandchild should be removed from child
+    assert child._children == []
+
+    root._clear_cache.assert_called_once()
+
+def test_remove_node_calls_cleanup_on_non_group_descendant(monkeypatch):
+    """_cleanup_descendants should early-return for non-group child nodes via remove_node."""
+
+    # Root must be a group
+    monkeypatch.setattr(DummyNode, "is_group_node", property(lambda _: True))
+
+    root = DummyNode(None)
+
+    # Child will be treated as NON-group
+    class NonGroupNode(DummyNode):
+        pass
+
+    child = NonGroupNode(root)
+    root._children = [child]
+
+    # Override is_group_node for child only
+    monkeypatch.setattr(NonGroupNode, "is_group_node", property(lambda _: False))
+
+    # Add a "grandchild" to prove no cleanup happens
+    grandchild = DummyNode(child)
+    child._children = [grandchild]
+
+    # Remove child ??? triggers _cleanup_descendants(child)
+    result = root.remove_node(child.id)
+
+    assert result is True
+
+    # This proves early-return happened:
+    # child._children should NOT have been cleared
+    assert child._children == [grandchild]
+
+def test_get_node_factory_runtime_error(monkeypatch):
+    """get_node should catch RuntimeError, log, and re-raise with wrapped message."""
+
+    # Force get_node_type_from_dataset to raise
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.base_node.VisorSceneGraphNode.get_node_type_from_dataset",
+        lambda dataset: (_ for _ in ()).throw(RuntimeError("original error"))
+    )
+
+    # Spy on logger
+    mock_logger = MagicMock()
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.base_node.logger",
+        mock_logger,
+    )
+
+    with pytest.raises(RuntimeError) as exc:
+        VisorSceneGraphNode.get_node(parent=None)
+
+    # Exception should be wrapped
+    assert "could not create scene graph node: original error" in str(exc.value)
+
+    # Logger should be called
+    mock_logger.error.assert_called_once()
+
diff --git a/tests/unit/vtk/scene_graph/test_group_node.py b/tests/unit/vtk/scene_graph/test_group_node.py
new file mode 100644
index 00000000..0556f52f
--- /dev/null
+++ b/tests/unit/vtk/scene_graph/test_group_node.py
@@ -0,0 +1,142 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from ansys.visor.viewer.vtk.scene_graph.group_node import VisorSceneGraphGroupNode
+
+
+@pytest.fixture
+def patch_vtk_types(monkeypatch):
+    """Provides dummy VTK types for testing."""
+    # Patch the VTK types to dummy classes
+    class DummyMultiBlock:
+        pass
+
+    class DummyMultiPiece:
+        pass
+
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.group_node.vtkMultiBlockDataSet",
+        DummyMultiBlock
+    )
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.group_node.vtkMultiPieceDataSet",
+        DummyMultiPiece
+    )
+    return DummyMultiBlock, DummyMultiPiece
+
+@pytest.fixture
+def mock_node_metadata():
+    """Provides a mock node metadata object."""
+    meta = MagicMock()
+    meta.Has.return_value = False
+    meta.Get.return_value = ""
+    return meta
+
+def test_init_with_multiblock(monkeypatch, patch_vtk_types, mock_node_metadata):
+    """Verify that initializing with multiblock dataset sets the correct properties."""
+    dummy_multiblock_cls, _ = patch_vtk_types
+    dummy_dataset = dummy_multiblock_cls()
+    dummy_dataset.GetNumberOfBlocks = lambda: 2
+    dummy_dataset.GetBounds = lambda arr: arr.__setitem__(slice(None), [1,2,3,4,5,6])
+    dummy_dataset.GetBlock = lambda i: f"block{i}"
+    dummy_dataset.GetMetaData = lambda i: f"meta{i}"
+    dummy_node = MagicMock()
+    monkeypatch.setattr("ansys.visor.viewer.vtk.scene_graph.VisorSceneGraphNode.get_node", lambda *a, **k: dummy_node)
+    node = VisorSceneGraphGroupNode(
+        parent=None,
+        node_metadata=mock_node_metadata,
+        dataset=dummy_dataset,
+        root_node=None
+    )
+    assert node._node_type.name == "GROUP"
+    assert node._vtk_dataset_type == "vtkMultiBlockDataSet"
+    assert node._bounds == [1, 2, 3, 4, 5, 6]
+    assert node.children == [dummy_node, dummy_node]
+
+def test_init_with_multipiece(monkeypatch, patch_vtk_types, mock_node_metadata):
+    """Verify that initializing with multipiece dataset sets the correct properties."""
+    _, dummy_multipiece_cls = patch_vtk_types
+    dummy_dataset = dummy_multipiece_cls()
+    dummy_dataset.GetNumberOfPieces = lambda: 1
+    dummy_dataset.GetBounds = lambda arr: arr.__setitem__(slice(None), [7,8,9,10,11,12])
+    dummy_dataset.GetBlock = lambda i: f"piece{i}"
+    dummy_dataset.GetMetaData = lambda i: f"meta{i}"
+    dummy_node = MagicMock()
+    monkeypatch.setattr("ansys.visor.viewer.vtk.scene_graph.VisorSceneGraphNode.get_node", lambda *a, **k: dummy_node)
+    node = VisorSceneGraphGroupNode(
+        parent=None,
+        node_metadata=mock_node_metadata,
+        dataset=dummy_dataset,
+        root_node=None
+    )
+    assert node._node_type.name == "GROUP"
+    assert node._vtk_dataset_type == "vtkMultiPieceDataSet"
+    assert node._bounds == [7, 8, 9, 10, 11, 12]
+    assert node.children == [dummy_node]
+
+def test_add_child_sets_parent_and_clears_cache(monkeypatch, patch_vtk_types, mock_node_metadata):
+    """Verify that adding child nodes sets parent and clears cache."""
+    dummy_multiblock_cls, _ = patch_vtk_types
+    dummy_dataset = dummy_multiblock_cls()
+    dummy_dataset.GetNumberOfBlocks = lambda: 0
+    dummy_dataset.GetBounds = lambda arr: arr.__setitem__(slice(None), [0]*6)
+    node = VisorSceneGraphGroupNode(
+        parent=None,
+        node_metadata=mock_node_metadata,
+        dataset=dummy_dataset,
+        root_node=None
+    )
+    child = MagicMock()
+    node._clear_cache = MagicMock()
+    node.add_child(child)
+    assert child.Parent is node
+    assert node.children == [child]
+    node._clear_cache.assert_called_once()
+
+def test_children_property(patch_vtk_types, mock_node_metadata):
+    """Verify that children property returns a list."""
+    dummy_multiblock_cls, _ = patch_vtk_types
+    dummy_dataset = dummy_multiblock_cls()
+    dummy_dataset.GetNumberOfBlocks = lambda: 0
+    dummy_dataset.GetBounds = lambda arr: arr.__setitem__(slice(None), [0]*6)
+    node = VisorSceneGraphGroupNode(
+        parent=None,
+        node_metadata=mock_node_metadata,
+        dataset=dummy_dataset,
+        root_node=None
+    )
+    assert isinstance(node.children, list)
+
+def test_state_property(patch_vtk_types, mock_node_metadata):
+    """Verify that state property returns a list."""
+    dummy_multiblock_cls, _ = patch_vtk_types
+    dummy_dataset = dummy_multiblock_cls()
+    dummy_dataset.GetNumberOfBlocks = lambda: 0
+    dummy_dataset.GetBounds = lambda arr: arr.__setitem__(slice(None), [0]*6)
+    node = VisorSceneGraphGroupNode(
+        parent=None,
+        node_metadata=mock_node_metadata,
+        dataset=dummy_dataset,
+        root_node=None
+    )
+    child = MagicMock()
+    child.state = "child_state"
+    node._children = [child]
+    state = node.state
+    assert hasattr(state, "children")
+    assert state.children == ["child_state"]
+
+def test_init_with_unsupported_type(mock_node_metadata):
+    """Verify that initializing with an unsupported dataset type raises RuntimeError."""
+    class DummyData:
+        pass
+
+    dummy = DummyData()
+    with pytest.raises(RuntimeError, match="node type not yet supported"):
+        VisorSceneGraphGroupNode(
+            parent=None,
+            node_metadata=mock_node_metadata,
+            dataset=dummy,
+            root_node=None
+        )
diff --git a/tests/unit/vtk/scene_graph/test_part_node.py b/tests/unit/vtk/scene_graph/test_part_node.py
new file mode 100644
index 00000000..5f00eaf6
--- /dev/null
+++ b/tests/unit/vtk/scene_graph/test_part_node.py
@@ -0,0 +1,143 @@
+"""Unit tests for VisorSceneGraphPartNode."""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+from vtkmodules.vtkCommonDataModel import vtkPolyData, vtkUnstructuredGrid
+from vtkmodules.vtkFiltersSources import vtkSphereSource
+
+from ansys.visor.viewer.core.visor_enums import VisorNodeType
+from ansys.visor.viewer.vtk.scene_graph.part_node import VisorSceneGraphPartNode
+
+
+@pytest.fixture
+def mock_all_init_methods():
+    """Mock non-VTK-pipeline init helpers to give stable behavior."""
+    vars_container = MagicMock()
+    vars_container.list_info.return_value = [{"dummy": "meta"}]
+
+    patches = [
+        patch.object(VisorSceneGraphPartNode, "_set_name"),
+        patch.object(VisorSceneGraphPartNode, "_get_vtk_dataset_type", return_value="vtkPolyData"),
+        patch.object(VisorSceneGraphPartNode, "_get_bounds", return_value=[0, 1, 2, 3, 4, 5]),
+        patch.object(VisorSceneGraphPartNode, "_get_variable_metadata", return_value=vars_container),
+    ]
+    for p in patches:
+        p.start()
+    yield
+    for p in patches:
+        p.stop()
+
+
+# ---------------------------------------------------------------------------
+# Structure and identity
+# ---------------------------------------------------------------------------
+
+def test_init_populates_scene_metadata_only(mock_all_init_methods):
+    dataset = MagicMock(name="dataset")
+    node = VisorSceneGraphPartNode(
+        parent=None,
+        node_metadata=MagicMock(),
+        dataset=dataset,
+    )
+
+    assert node._node_type == VisorNodeType.PART
+    assert node.dataset is dataset
+    assert node._vtk_dataset_type == "vtkPolyData"
+    assert node._bounds == [0, 1, 2, 3, 4, 5]
+    assert node.data_arrays == [{"dummy": "meta"}]
+
+
+def test_part_node_holds_no_vtk_pipeline_objects(mock_all_init_methods):
+    """Guard rail: pipeline objects must never appear on the part node."""
+    node = VisorSceneGraphPartNode(
+        parent=None,
+        node_metadata=MagicMock(),
+        dataset=MagicMock(),
+    )
+    for banned in ("_Actor", "_Mapper", "_BaseAlgorithm", "Actor", "Mapper", "BaseAlgorithm"):
+        assert not hasattr(node, banned), (
+            f"VisorSceneGraphPartNode must not expose {banned!r}: "
+            "VTK pipeline objects belong on VtkNodePipeline."
+        )
+
+
+def test_is_part_node_true(mock_all_init_methods):
+    node = VisorSceneGraphPartNode(
+        parent=None, node_metadata=MagicMock(), dataset=MagicMock()
+    )
+    assert node.is_part_node is True
+    assert node.is_group_node is False
+
+
+# ---------------------------------------------------------------------------
+# Wire-format identity: node.state must serialize the same way an actor node
+# would (isActorNode=True, dataArrays populated, diffuseColor default).
+# ---------------------------------------------------------------------------
+
+def test_state_matches_wire_format_for_actor_leaf(mock_all_init_methods):
+    node = VisorSceneGraphPartNode(
+        parent=None, node_metadata=MagicMock(), dataset=MagicMock()
+    )
+    state = node.state
+
+    assert state.is_actor_node is True
+    assert state.is_group_node is False
+    assert state.data_arrays == [{"dummy": "meta"}]
+    assert state.bounds == [0, 1, 2, 3, 4, 5]
+    assert state.node_type == "vtkPolyData"
+    assert hasattr(state, "diffuse_color")
+
+
+# ---------------------------------------------------------------------------
+# Variable metadata refresh does not touch a pipeline
+# ---------------------------------------------------------------------------
+
+def test_refresh_variable_metadata_rereads_from_dataset(mock_all_init_methods):
+    node = VisorSceneGraphPartNode(
+        parent=None, node_metadata=MagicMock(), dataset=MagicMock()
+    )
+    with patch.object(
+        VisorSceneGraphPartNode,
+        "_get_variable_metadata",
+        return_value=MagicMock(list_info=lambda: [{"refreshed": True}]),
+    ) as m:
+        node.refresh_variable_metadata()
+        m.assert_called_once_with(node._dataset)
+    assert node.data_arrays == [{"refreshed": True}]
+
+
+# ---------------------------------------------------------------------------
+# Dataset-type dispatch (real VTK types, not mocked)
+# ---------------------------------------------------------------------------
+
+def _sphere_polydata() -> vtkPolyData:
+    src = vtkSphereSource()
+    src.Update()
+    return src.GetOutput()
+
+
+def test_get_vtk_dataset_type_polydata():
+    info = MagicMock()
+    info.Has.return_value = False
+    node = VisorSceneGraphPartNode(
+        parent=None, node_metadata=info, dataset=_sphere_polydata()
+    )
+    assert node._vtk_dataset_type == "vtkPolyData"
+
+
+def test_get_vtk_dataset_type_unstructured_grid():
+    info = MagicMock()
+    info.Has.return_value = False
+    node = VisorSceneGraphPartNode(
+        parent=None, node_metadata=info, dataset=vtkUnstructuredGrid()
+    )
+    assert node._vtk_dataset_type == "vtkUnstructuredGrid"
+
+
+def test_get_vtk_dataset_type_unsupported_raises():
+    info = MagicMock()
+    info.Has.return_value = False
+    with pytest.raises(RuntimeError, match="node type not yet supported"):
+        VisorSceneGraphPartNode(parent=None, node_metadata=info, dataset=object())
+
diff --git a/tests/unit/vtk/scene_graph/test_root_node.py b/tests/unit/vtk/scene_graph/test_root_node.py
new file mode 100644
index 00000000..9d9e2291
--- /dev/null
+++ b/tests/unit/vtk/scene_graph/test_root_node.py
@@ -0,0 +1,90 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from ansys.visor.viewer.vtk.scene_graph.root_node import VisorBounds, VisorSceneGraph
+
+
+def test_visor_bounds_set_and_remove():
+    """Verify that VisorBounds set and remove methods work correctly."""
+    bounds = VisorBounds()
+    bounds.set(1, [1,2,3,4,5,6])
+    assert bounds.bounds[1] == [1,2,3,4,5,6]
+    bounds.remove(1)
+    assert 1 not in bounds.bounds
+
+def test_visor_bounds_compute_bounds_empty():
+    """Verify that VisorBounds compute bounds correctly."""
+    bounds = VisorBounds()
+    result = bounds.compute_bounds()
+    assert result == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+
+def test_visor_bounds_compute_bounds_multiple():
+    """Verify that VisorBounds compute bounds correctly with multiple entries."""
+    bounds = VisorBounds()
+    bounds.set(1, [1,2,3,4,5,6])
+    bounds.set(2, [0,3,2,5,1,7])
+    result = bounds.compute_bounds()
+    # min_x=0, max_x=3, min_y=2, max_y=5, min_z=1, max_z=7
+    assert result == [0,3,2,5,1,7]
+
+@pytest.fixture
+def patch_scene_graph_node(monkeypatch):
+    """Verify that scene graph node is set correctly."""
+    dummy_node = MagicMock()
+    dummy_node.id = 42
+    dummy_node.bounds = [1,2,3,4,5,6]
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.VisorSceneGraphNode.get_node",
+        lambda *a, **k: dummy_node
+    )
+    return dummy_node
+
+@pytest.fixture
+def patch_vtk_information(monkeypatch):
+    """Provide a mock for vtkInformation to avoid VTK dependency in tests."""
+    monkeypatch.setattr(
+        "ansys.visor.viewer.vtk.scene_graph.root_node.vtkInformation",
+        MagicMock
+    )
+
+def test_scene_graph_init(patch_vtk_information):
+    """Verify that VisorSceneGraph initializes correctly with expected properties."""
+    graph = VisorSceneGraph()
+    assert graph._node_type.name == "ROOT"
+    assert graph._name == "root"
+    assert graph._vtk_dataset_type == "root"
+    assert isinstance(graph._dataset_bounds, VisorBounds)
+
+def test_scene_graph_bounds_property(patch_vtk_information):
+    """Verify that VisorSceneGraph bounds property reflects dataset bounds correctly."""
+    graph = VisorSceneGraph()
+    # No bounds set yet
+    assert graph.bounds == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    # Set some bounds
+    graph._dataset_bounds.set(1, [1,2,3,4,5,6])
+    assert graph.bounds == [1, 2, 3, 4, 5, 6]
+
+def test_scene_graph_load_dataset(patch_scene_graph_node, patch_vtk_information):
+    """Verify that scene graph load_dataset correctly with expected properties."""
+    graph = VisorSceneGraph()
+    dummy_dataset = MagicMock()
+    node_id = graph.load_dataset(dummy_dataset, name="foo")
+    # Should call get_node and add_child, and update bounds
+    assert node_id == 42
+    assert graph._dataset_bounds.bounds[42] == [1,2,3,4,5,6]
+    assert graph.children[-1].id == 42
+
+def test_scene_graph_remove_dataset(patch_scene_graph_node, patch_vtk_information):
+    """Verify that scene graph remove_dataset correctly removes the dataset and updates bounds."""
+    graph = VisorSceneGraph()
+    dummy_dataset = MagicMock()
+    node_id = graph.load_dataset(dummy_dataset, name="foo")
+    # Add another bounds for coverage
+    graph._dataset_bounds.set(99, [0,1,2,3,4,5])
+    # Remove the loaded node
+    graph.remove_dataset(node_id)
+    assert node_id not in graph._dataset_bounds.bounds
+    # Remove a non-existent node (should not raise)
+    graph.remove_dataset(123)
+
diff --git a/tests/unit/vtk/scene_graph/test_state.py b/tests/unit/vtk/scene_graph/test_state.py
new file mode 100644
index 00000000..a55a99c8
--- /dev/null
+++ b/tests/unit/vtk/scene_graph/test_state.py
@@ -0,0 +1,55 @@
+import pydantic
+import pytest
+
+from ansys.visor.viewer.core.visor_colors import VisorColors
+from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo
+
+
+def test_minimal_construction():
+    """Verify that a SceneGraphNodeInfo can be constructed with minimal required fields."""
+    node = SceneGraphNodeInfo(
+        id=1,
+        name="root",
+        is_actor_node=True,
+        is_group_node=False,
+        node_type="actor"
+    )
+    assert node.id == 1
+    assert node.name == "root"
+    assert node.diffuse_color == VisorColors.DefaultMeshColor
+    assert node.children == []
+
+def test_defaults_and_nested_children():
+    """Verify that default values are applied and children can be nested."""
+    child = SceneGraphNodeInfo(
+        id=2,
+        name="child",
+        is_actor_node=False,
+        is_group_node=True,
+        node_type="group"
+    )
+    parent = SceneGraphNodeInfo(
+        id=1,
+        name="parent",
+        is_actor_node=True,
+        is_group_node=False,
+        node_type="actor",
+        children=[child]
+    )
+    assert parent.children[0].name == "child"
+
+def test_missing_required_fields():
+    """Verify a ValidationError is raised when required fields are missing from SceneGraphNodeInfo."""
+    with pytest.raises(pydantic.ValidationError):
+        SceneGraphNodeInfo(id=1)  # missing required fields
+
+def test_type_validation():
+    """Verify that type validation works for SceneGraphNodeInfo."""
+    with pytest.raises(ValueError):
+        SceneGraphNodeInfo(
+            id="not-an-int",
+            name="bad",
+            is_actor_node=True,
+            is_group_node=False,
+            node_type="actor"
+        )
diff --git a/tests/unit/vtk/test_node_pipeline.py b/tests/unit/vtk/test_node_pipeline.py
new file mode 100644
index 00000000..8443ca7a
--- /dev/null
+++ b/tests/unit/vtk/test_node_pipeline.py
@@ -0,0 +1,134 @@
+"""Unit tests for VtkNodePipeline."""
+
+from unittest.mock import MagicMock
+
+import pytest
+from vtkmodules.vtkCommonDataModel import vtkPlane, vtkPolyData, vtkUnstructuredGrid
+from vtkmodules.vtkFiltersCore import vtkAppendPolyData
+from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter
+from vtkmodules.vtkFiltersSources import vtkSphereSource
+from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper
+
+from ansys.visor.viewer.core.visor_colors import VisorColors
+from ansys.visor.viewer.vtk.node_pipeline import VtkNodePipeline
+
+
+@pytest.fixture
+def poly_dataset() -> vtkPolyData:
+    src = vtkSphereSource()
+    src.Update()
+    return src.GetOutput()
+
+
+@pytest.fixture
+def unstructured_dataset() -> vtkUnstructuredGrid:
+    # Minimal empty vtkUnstructuredGrid is sufficient for pipeline wiring tests.
+    return vtkUnstructuredGrid()
+
+
+# ---------------------------------------------------------------------------
+# from_dataset
+# ---------------------------------------------------------------------------
+
+def test_from_dataset_polydata_builds_append_algorithm(poly_dataset):
+    pipe = VtkNodePipeline.from_dataset(poly_dataset)
+
+    assert isinstance(pipe.base_algorithm, vtkAppendPolyData)
+    assert isinstance(pipe.mapper, vtkPolyDataMapper)
+    assert isinstance(pipe.actor, vtkActor)
+    assert pipe.actor.GetMapper() is pipe.mapper
+    # Mapper is wired to the base algorithm's output port.
+    assert pipe.mapper.GetInputConnection(0, 0).GetProducer() is pipe.base_algorithm
+
+
+def test_from_dataset_unstructured_builds_geometry_filter(unstructured_dataset):
+    pipe = VtkNodePipeline.from_dataset(unstructured_dataset)
+
+    assert isinstance(pipe.base_algorithm, vtkGeometryFilter)
+    assert pipe.base_algorithm.GetNonlinearSubdivisionLevel() == 0
+    assert pipe.mapper.GetInputConnection(0, 0).GetProducer() is pipe.base_algorithm
+
+
+def test_from_dataset_sets_default_color_and_flat_shading(poly_dataset):
+    pipe = VtkNodePipeline.from_dataset(poly_dataset)
+
+    diffuse = pipe.actor.GetProperty().GetDiffuseColor()
+    assert tuple(diffuse) == tuple(VisorColors.DefaultMeshColor)
+    # Interpolation 0 == flat shading in VTK.
+    assert pipe.actor.GetProperty().GetInterpolation() == 0
+
+
+def test_from_dataset_unsupported_type_raises():
+    with pytest.raises(RuntimeError, match="node type not yet supported"):
+        VtkNodePipeline.from_dataset(object())
+
+
+# ---------------------------------------------------------------------------
+# set_clipping_plane
+# ---------------------------------------------------------------------------
+
+def test_set_clipping_plane_none_clears_planes():
+    mapper = MagicMock()
+    pipe = VtkNodePipeline(actor=MagicMock(), mapper=mapper, base_algorithm=MagicMock())
+
+    pipe.set_clipping_plane(None)
+
+    mapper.RemoveAllClippingPlanes.assert_called_once()
+    mapper.AddClippingPlane.assert_not_called()
+
+
+def test_set_clipping_plane_with_plane_adds_it():
+    mapper = MagicMock()
+    pipe = VtkNodePipeline(actor=MagicMock(), mapper=mapper, base_algorithm=MagicMock())
+    plane = vtkPlane()
+
+    pipe.set_clipping_plane(plane)
+
+    mapper.RemoveAllClippingPlanes.assert_called_once()
+    mapper.AddClippingPlane.assert_called_once_with(plane)
+
+
+def test_set_clipping_plane_end_to_end_on_real_mapper(poly_dataset):
+    pipe = VtkNodePipeline.from_dataset(poly_dataset)
+    plane = vtkPlane()
+
+    pipe.set_clipping_plane(plane)
+    assert pipe.mapper.GetNumberOfClippingPlanes() == 1
+
+    pipe.set_clipping_plane(None)
+    assert pipe.mapper.GetNumberOfClippingPlanes() == 0
+
+
+# ---------------------------------------------------------------------------
+# update_input
+# ---------------------------------------------------------------------------
+
+def test_update_input_without_filter_reconnects_base(poly_dataset):
+    pipe = VtkNodePipeline.from_dataset(poly_dataset)
+
+    # Point the mapper somewhere else, then call update_input(None) and confirm
+    # it reconnects to the base algorithm.
+    other = vtkAppendPolyData()
+    other.SetInputData(poly_dataset)
+    pipe.mapper.SetInputConnection(other.GetOutputPort())
+    assert pipe.mapper.GetInputConnection(0, 0).GetProducer() is other
+
+    pipe.update_input(None)
+
+    assert pipe.mapper.GetInputConnection(0, 0).GetProducer() is pipe.base_algorithm
+
+
+def test_update_input_with_filter_uses_returned_algorithm(poly_dataset):
+    pipe = VtkNodePipeline.from_dataset(poly_dataset)
+    wrapper = vtkAppendPolyData()
+
+    def algorithm_filter(base):
+        # Chain: base -> wrapper
+        wrapper.SetInputConnection(base.GetOutputPort())
+        return wrapper
+
+    pipe.update_input(algorithm_filter)
+
+    assert pipe.mapper.GetInputConnection(0, 0).GetProducer() is wrapper
+
+
diff --git a/tests/unit/vtk/test_variable_type_parity.py b/tests/unit/vtk/test_variable_type_parity.py
new file mode 100644
index 00000000..acb4b04a
--- /dev/null
+++ b/tests/unit/vtk/test_variable_type_parity.py
@@ -0,0 +1,54 @@
+"""Cross-stack parity test: the Python ``VisorVtkVariableType`` enum's
+member values must agree with the frontend's ``FIELD_ASSOCIATIONS`` literal
+in ``VisorVtkDataArray.tsx``.
+
+The TypeScript file is read as text and parsed for its ``FIELD_ASSOCIATIONS``
+literal -- it is not executed (no jest/node toolchain is invoked here); this
+is the "stub" side of the cross-stack check. If the file cannot be found,
+this test fails loudly (no ``skipif``/try-except swallowing the error).
+"""
+
+import re
+from pathlib import Path
+
+from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType
+
+_TSX_RELATIVE_PATH = (
+    "src/ansys/visor/visor-client/src/state/appstate/vtkInfo/VisorVtkDataArray.tsx"
+)
+
+
+def _repo_root() -> Path:
+    """Walk up from this file until a directory containing the TS file's
+    root-relative path is found."""
+    here = Path(__file__).resolve()
+    for parent in here.parents:
+        candidate = parent / _TSX_RELATIVE_PATH
+        if candidate.is_file():
+            return parent
+    raise FileNotFoundError(
+        f"Could not locate {_TSX_RELATIVE_PATH!r} above {here}"
+    )
+
+
+def _extract_field_associations(text: str) -> set:
+    match = re.search(r"const\s+FIELD_ASSOCIATIONS\s*=\s*\[([^]]*)\]", text)
+    assert match, "FIELD_ASSOCIATIONS literal not found in VisorVtkDataArray.tsx"
+    items = re.findall(r"'([^']*)'|\"([^\"]*)\"", match.group(1))
+    return {a or b for a, b in items}
+
+
+def test_python_enum_values_match_frontend_field_associations():
+    tsx_path = _repo_root() / _TSX_RELATIVE_PATH
+    text = tsx_path.read_text(encoding="utf-8")
+
+    extracted = _extract_field_associations(text)
+
+    # The literal must actually contain entries, and the stated size (2:
+    # POINT, CELL) must hold, before comparing set contents.
+    assert len(extracted) > 0
+    assert len(extracted) == 2
+
+    python_values = {member.value for member in VisorVtkVariableType}
+    assert python_values == extracted
+
diff --git a/tests/unit/vtk/test_wire_format_identity.py b/tests/unit/vtk/test_wire_format_identity.py
new file mode 100644
index 00000000..22e3df96
--- /dev/null
+++ b/tests/unit/vtk/test_wire_format_identity.py
@@ -0,0 +1,341 @@
+"""End-to-end wire-format identity test for the split scene graph.
+
+Exercises the full path
+    VisorSceneGraph.load_dataset(...)
+        -> VisorSceneGraphPartNode
+        -> VtkNodePipeline.from_dataset(part.dataset)
+        -> VisorSceneBase.get_scene_details()
+           -> _build_scene_graph_state() -> SceneGraphStateBuilder.build()
+        -> SceneGraphNodeInfo tree
+
+and asserts the emitted payload has the shape and value types this story's
+split produces: renderer handles live only on ``vtkInfo.rendererAnnotation``,
+never on ``sceneGraph``.
+"""
+
+import json
+import re
+from unittest.mock import MagicMock
+
+from vtkmodules.vtkCommonCore import vtkFloatArray
+from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet, vtkPolyData
+from vtkmodules.vtkFiltersSources import vtkSphereSource
+
+from ansys.visor.viewer.core.visor_colors import VisorColors
+from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import (
+    WasmNodeHandles,
+    WasmRendererAnnotation,
+    WasmWidgetHandles,
+)
+from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo
+from ansys.visor.viewer.vtk.node_pipeline import VtkNodePipeline
+from ansys.visor.viewer.vtk.scene.base import VisorSceneBase
+from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraph
+
+
+def _sphere_polydata() -> vtkPolyData:
+    src = vtkSphereSource()
+    src.Update()
+    return src.GetOutput()
+
+
+def _sphere_polydata_with_point_and_cell_arrays() -> vtkPolyData:
+    """Sphere polydata carrying one real point-data array and one real
+    cell-data array, so the emitted ``dataArrays`` wire payload is non-empty
+    and exercises both ``VisorVtkVariableType`` members end to end."""
+    poly = _sphere_polydata()
+
+    point_array = vtkFloatArray()
+    point_array.SetName("PointScalar")
+    point_array.SetNumberOfComponents(1)
+    point_array.SetNumberOfTuples(poly.GetNumberOfPoints())
+    for i in range(poly.GetNumberOfPoints()):
+        point_array.SetValue(i, float(i))
+    poly.GetPointData().AddArray(point_array)
+
+    cell_array = vtkFloatArray()
+    cell_array.SetName("CellScalar")
+    cell_array.SetNumberOfComponents(1)
+    cell_array.SetNumberOfTuples(poly.GetNumberOfCells())
+    for i in range(poly.GetNumberOfCells()):
+        cell_array.SetValue(i, float(i))
+    poly.GetCellData().AddArray(cell_array)
+
+    return poly
+
+
+
+class _CountingObjectManager:
+    """Assigns a stable, unique positive id per unique VTK object.
+
+    Mirrors the observed behavior of ``vtkObjectManager``.
+    """
+
+    def __init__(self):
+        self._ids: dict[int, int] = {}
+        self._next = 1
+
+    def GetId(self, obj) -> int: # noqa: N802
+        key = id(obj)
+        if key not in self._ids:
+            self._ids[key] = self._next
+            self._next += 1
+        return self._ids[key]
+
+
+class _StubRenderer:
+    """Minimal ``IRenderer`` stand-in.
+
+    Builds a real ``WasmRendererAnnotation`` from a real pipeline registry
+    and a real (counting) object-manager-like id source -- the same
+    construction :class:`VisorLocalRenderer.build_renderer_annotation` does.
+    This is the one unavoidable test double a call to the real
+    ``VisorSceneBase.get_scene_details()`` needs; nothing about scene-graph
+    building or wasm-id stamping is reimplemented here -- both happen inside
+    ``get_scene_details()`` itself.
+    """
+
+    def __init__(self, pipelines, object_manager):
+        self._pipelines = pipelines
+        self._object_manager = object_manager
+
+    def build_renderer_annotation(self) -> WasmRendererAnnotation:
+        get_id = self._object_manager.GetId
+        nodes = {
+            str(node_id): WasmNodeHandles(
+                actor_id=get_id(pipe.actor),
+                property_id=get_id(pipe.actor.GetProperty()),
+                mapper_id=get_id(pipe.mapper),
+            )
+            for node_id, pipe in self._pipelines.items()
+        }
+        widgets = WasmWidgetHandles(
+            orientation_widget_id=1,
+            cross_section_plane_id=2,
+            cross_section_plane_widget_id=3,
+            cross_section_plane_representation_id=4,
+            bounding_box_algorithm_id=5,
+            bounding_box_outline_actor_id=6,
+            bounding_box_axes_actor_id=7,
+        )
+        return WasmRendererAnnotation(nodes=nodes, widgets=widgets)
+
+
+class _WireFormatScene(VisorSceneBase):
+    """Minimal concrete ``VisorSceneBase`` for exercising the real
+    ``get_scene_details()`` path end to end. The two abstract hooks are
+    unused by that path and are never called by this test."""
+
+    async def _get_runtime_state_async(self, timeout: float):
+        raise NotImplementedError
+
+    def _apply_runtime_state_to_render(self, runtime_app_state) -> None:
+        raise NotImplementedError
+
+
+def _build_scene(dataset) -> _WireFormatScene:
+    """Load ``dataset``, spin up real pipelines, and return the fully wired
+    ``_WireFormatScene`` (a real, unmodified ``VisorSceneBase``) so callers
+    can exercise either ``get_scene_details()`` or ``get_scene_details_json()``
+    -- the same production entry point."""
+    graph = VisorSceneGraph()
+    graph.load_dataset(dataset, name="ds")
+
+    # Simulate VisorSceneBase's pipeline registry: one VtkNodePipeline per leaf.
+    pipelines = {
+        part.id: VtkNodePipeline.from_dataset(part.dataset)
+        for part in graph.get_descendant_part_nodes(include_self=False)
+    }
+    assert pipelines, "test setup: expected at least one part node in graph"
+
+    object_manager = _CountingObjectManager()
+    renderer = _StubRenderer(pipelines, object_manager)
+
+    scene = _WireFormatScene(server=MagicMock(), renderer=renderer)
+    scene._scene_graph = graph
+
+    return scene
+
+
+def _build_full_scene_details(dataset):
+    """Load ``dataset`` and return the ``VisorSceneDetails`` produced by the
+    real, unmodified ``VisorSceneBase.get_scene_details()`` path."""
+    return _build_scene(dataset).get_scene_details()
+
+
+
+def _build_end_to_end_payload(dataset) -> SceneGraphNodeInfo:
+    """Load ``dataset`` and return the ``SceneGraphNodeInfo`` produced by the
+    real, unmodified ``VisorSceneBase.get_scene_details()`` path."""
+    return _build_full_scene_details(dataset).vtk_info.scene_graph
+
+
+
+# ---------------------------------------------------------------------------
+# Wire-format field inventory (frozen contract with the frontend).
+# ---------------------------------------------------------------------------
+
+# These are the fields SceneGraphNodeInfo exposes on ``main``. Any change to
+# this set is a wire-format break and must be a coordinated frontend change
+_WIRE_FIELDS = {
+    "id", "name", "isActorNode", "isGroupNode",
+    "dataArrays", "nodeType", "bounds", "children", "diffuseColor",
+}
+
+
+def _assert_wire_shape(info: SceneGraphNodeInfo) -> None:
+    """Every node in the tree must expose exactly the frozen field set."""
+    d = info.model_dump(by_alias=True)
+    assert set(d.keys()) == _WIRE_FIELDS, (
+        f"wire-format drift: got {sorted(d.keys())}, expected {sorted(_WIRE_FIELDS)}"
+    )
+    for child in info.children:
+        _assert_wire_shape(child)
+
+
+# ---------------------------------------------------------------------------
+# Single-part scene (vtkPolyData sphere loaded as a leaf).
+# ---------------------------------------------------------------------------
+
+def test_single_part_payload_shape_and_types():
+    poly = _sphere_polydata()
+    out = _build_end_to_end_payload(poly)
+
+    # Root
+    _assert_wire_shape(out)
+    assert out.name == "root"
+    assert out.node_type == "root"
+    assert out.is_group_node is True
+    assert out.is_actor_node is False
+    assert len(out.children) == 1
+
+    # Dataset "group" wrapping the loaded polydata (single-leaf load creates a
+    # part node directly as the dataset child, since polydata is not composite).
+    part = out.children[0]
+    assert part.is_actor_node is True  # wire-format contract preserved
+    assert part.is_group_node is False
+    assert part.node_type == "vtkPolyData"
+    assert part.children == []
+    assert isinstance(part.bounds, list) and len(part.bounds) == 6
+
+    # dataArrays is a list (possibly empty for a raw sphere).
+    assert isinstance(part.data_arrays, list)
+
+    # diffuseColor is the Visor default.
+    assert part.diffuse_color == VisorColors.DefaultMeshColor
+
+
+# ---------------------------------------------------------------------------
+# Composite (multi-block) scene: exercises group recursion.
+# ---------------------------------------------------------------------------
+
+def _two_block_dataset() -> vtkMultiBlockDataSet:
+    mb = vtkMultiBlockDataSet()
+    mb.SetNumberOfBlocks(2)
+    mb.SetBlock(0, _sphere_polydata())
+    mb.SetBlock(1, _sphere_polydata())
+    return mb
+
+
+def test_multiblock_payload_recurses_through_groups():
+    mb = _two_block_dataset()
+    out = _build_end_to_end_payload(mb)
+
+    _assert_wire_shape(out)
+    assert out.is_group_node is True
+    # root -> multiblock-group -> [part, part]
+    (group,) = out.children
+    assert group.is_group_node is True
+    assert group.is_actor_node is False
+    assert group.node_type == "vtkMultiBlockDataSet"
+    assert len(group.children) == 2
+
+    for leaf in group.children:
+        assert leaf.is_actor_node is True
+        assert leaf.is_group_node is False
+        assert leaf.node_type == "vtkPolyData"
+
+
+# ---------------------------------------------------------------------------
+# JSON round-trip: proves the payload is fully JSON-serialisable, which is
+# what actually ships over the wire.
+# ---------------------------------------------------------------------------
+
+def test_payload_is_json_serialisable():
+    out = _build_end_to_end_payload(_sphere_polydata())
+    text = json.dumps(out.model_dump(by_alias=True))
+    assert '"isActorNode"' in text
+    # Round-trip parse.
+    parsed = json.loads(text)
+    assert parsed["name"] == "root"
+    assert parsed["children"][0]["isActorNode"] is True
+
+
+# ---------------------------------------------------------------------------
+# The payload splits into a scene description and a renderer
+# annotation, and no wasm*Id key appears anywhere under sceneGraph. Recursive
+# sweep, not a spot check.
+# ---------------------------------------------------------------------------
+
+_WASM_ID_KEY_RE = re.compile(r"^wasm[A-Za-z]*Id$")
+
+
+def _find_wasm_id_keys(obj, path: str = "") -> list:
+    """Recursively collect every dict key that looks like a legacy
+    ``wasm*Id`` renderer handle, anywhere in a JSON-shaped structure."""
+    hits = []
+    if isinstance(obj, dict):
+        for key, value in obj.items():
+            key_path = f"{path}.{key}" if path else key
+            if _WASM_ID_KEY_RE.match(key):
+                hits.append(key_path)
+            hits.extend(_find_wasm_id_keys(value, key_path))
+    elif isinstance(obj, list):
+        for i, item in enumerate(obj):
+            hits.extend(_find_wasm_id_keys(item, f"{path}[{i}]"))
+    return hits
+
+
+def test_payload_splits_scene_graph_and_renderer_annotation_with_no_wasm_ids_under_scene_graph():
+    scene = _build_scene(_sphere_polydata())
+    # Call the real production payload path -- get_scene_details_json() itself
+    # does `json.dumps(self.get_scene_details().model_dump(exclude_none=True,
+    # by_alias=True))` (vtk/scene/base.py) -- so a later change to those dump
+    # kwargs is caught here rather than in a re-derived copy.
+    payload = json.loads(scene.get_scene_details_json())
+
+    assert "sceneGraph" in payload["vtkInfo"]
+    assert "rendererAnnotation" in payload["vtkInfo"]
+    assert _find_wasm_id_keys(payload["vtkInfo"]["sceneGraph"]) == []
+
+
+# ---------------------------------------------------------------------------
+# The `type` field of an emitted dataArrays entry is the plain wire
+# string ("POINT"/"CELL"), not the VisorVtkVariableType enum member, in the
+# actual emitted bytes -- exercised through the real get_scene_details_json()
+# production path, not a re-derived model_dump call.
+# ---------------------------------------------------------------------------
+
+def _collect_data_arrays(node: dict) -> list:
+    """Recursively collect every ``dataArrays`` entry under a sceneGraph node."""
+    hits = list(node.get("dataArrays", []))
+    for child in node.get("children", []):
+        hits.extend(_collect_data_arrays(child))
+    return hits
+
+
+def test_data_array_type_is_str_and_point_or_cell_in_emitted_json():
+    scene = _build_scene(_sphere_polydata_with_point_and_cell_arrays())
+    payload = json.loads(scene.get_scene_details_json())
+
+    all_data_arrays = _collect_data_arrays(payload["vtkInfo"]["sceneGraph"])
+
+    # An empty dataArrays list cannot satisfy either assertion below.
+    assert any(d["type"] == "POINT" for d in all_data_arrays)
+    assert any(d["type"] == "CELL" for d in all_data_arrays)
+
+    for d in all_data_arrays:
+        assert isinstance(d["type"], str)
+        assert d["type"] in {"POINT", "CELL"}
+
+
diff --git a/tests/unit/vtk/variables/test_visor_update_variable.py b/tests/unit/vtk/variables/test_visor_update_variable.py
new file mode 100644
index 00000000..233b1752
--- /dev/null
+++ b/tests/unit/vtk/variables/test_visor_update_variable.py
@@ -0,0 +1,203 @@
+# python
+from dataclasses import FrozenInstanceError
+from enum import Enum
+
+import numpy as np
+import pytest
+
+# Module under test
+from ansys.visor.viewer.vtk.variables import visor_variable_update as tvu
+
+
+@pytest.fixture(autouse=True)
+def patch_enum(monkeypatch):
+    """Providdes a fake enum for testing, and patches the module to use it."""
+    class FakeEnum(Enum):
+        POINT = "POINT"
+        CELL = "CELL"
+
+    # Replace the enum used by the module so we do not depend on external code
+    monkeypatch.setattr(tvu, "VisorVtkVariableType", FakeEnum, raising=True)
+    return FakeEnum
+
+
+# ---------- Constructor (__post_init__) tests ----------
+
+def test_init_with_enum_success(patch_enum):
+    """Verify that initializing with valid enum succeeds."""
+    v = tvu.VisorVariableUpdate(
+        type=patch_enum.POINT,
+        name="var",
+        num_components=2,
+        data=[1, 2, 3, 4],
+    )
+    assert v.type is patch_enum.POINT
+    assert v.name == "var"
+    assert v.num_components == 2
+    # Constructor does not reshape/convert data
+    assert v.data == [1, 2, 3, 4]
+
+
+@pytest.mark.parametrize("type_value, expected", [("POINT", "POINT"), ("point", "POINT"), ("cell", "CELL")])
+def test_init_with_string_type_coercion(type_value, expected, patch_enum):
+    """Verify that initializing with string type coercion succeeds."""
+    v = tvu.VisorVariableUpdate(
+        type=type_value,
+        name="n",
+        num_components=1,
+        data=[1],
+    )
+    assert v.type is getattr(patch_enum, expected)
+
+
+@pytest.mark.parametrize("bad_type", ["unknown", "not_an_enum"])
+def test_init_with_invalid_type_raises(bad_type):
+    """Verify that initializing with invalid type raises ValueError."""
+    with pytest.raises(ValueError, match="Invalid type"):
+        tvu.VisorVariableUpdate(type=bad_type, name="n", num_components=1, data=[1])
+
+
+@pytest.mark.parametrize("bad_name", ["", None])
+def test_init_with_bad_name_raises(bad_name):
+    """Verify that initializing with bad name raises ValueError."""
+    with pytest.raises(ValueError, match="name must be non-empty"):
+        tvu.VisorVariableUpdate(type="POINT", name=bad_name, num_components=1, data=[1])
+
+
+@pytest.mark.parametrize("bad_nc", [0, -1])
+def test_init_with_non_positive_num_components_raises(bad_nc):
+    """Verify that initializing with non-positive num_components raises ValueError."""
+    with pytest.raises(ValueError, match="num_components must be > 0"):
+        tvu.VisorVariableUpdate(type="POINT", name="n", num_components=bad_nc, data=[1])
+
+
+def test_frozen_dataclass_cannot_be_modified(patch_enum):
+    """Verify that VisorVariableUpdate is frozen and cannot be modified after creation."""
+    v = tvu.VisorVariableUpdate(type=patch_enum.CELL, name="n", num_components=1, data=[1])
+    with pytest.raises(FrozenInstanceError):
+        v.name = "other"
+
+
+# ---------- from_dict tests (covers data validation/reshaping via public API) ----------
+
+def test_from_dict_valid_with_enum_type_and_list_1d():
+    """Verify that from_dict works with valid enum type and 1D list data."""
+    d = {
+        "type": "POINT",
+        "name": "pressure",
+        "num_components": 3,
+        "data": [1, 2, 3, 4, 5, 6],
+    }
+    v = tvu.VisorVariableUpdate.from_dict(d)
+    assert v.name == "pressure"
+    assert v.num_components == 3
+    assert isinstance(v.data, np.ndarray)
+    assert v.data.shape == (2, 3)
+    assert v.data.dtype == np.float32
+
+
+def test_from_dict_valid_with_lowercase_type_and_numpy_1d_nc1():
+    """Verify that from_dict works with lowercase type and 1D numpy array with num_components=1."""
+    arr = np.array([10, 20, 30], dtype=np.float64)
+    v = tvu.VisorVariableUpdate.from_dict(
+        {"type": "cell", "name": "scalar", "num_components": 1, "data": arr}
+    )
+    assert v.data.shape == (3, 1)
+    assert v.data.dtype == np.float32
+
+
+def test_from_dict_numpy_1d_multicomponent_raises():
+    """Verify that from_dict raises ValueError when a 1D numpy array is provided for multi-component data."""
+    arr = np.array([1, 2, 3], dtype=np.float32)
+    with pytest.raises(ValueError, match="1D NumPy array provided"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": "v", "num_components": 2, "data": arr}
+        )
+
+
+def test_from_dict_numpy_2d_last_dim_mismatch_raises():
+    """Verify that from_dict raises ValueError when a 2D numpy array's last dimension does not match num_components."""
+    arr = np.zeros((4, 2), dtype=np.float32)  # last dim=2
+    with pytest.raises(ValueError, match="last dimension .* does not match"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": "v", "num_components": 3, "data": arr}
+        )
+
+
+def test_from_dict_numpy_3d_flattens_to_neg1_nc_and_casts_dtype():
+    """Verify that from_dict flattens a 3D numpy array to (-1, num_components) and casts dtype to float32."""
+    arr = np.zeros((2, 5, 3), dtype=np.float64)  # last dim=3
+    v = tvu.VisorVariableUpdate.from_dict(
+        {"type": "POINT", "name": "vec", "num_components": 3, "data": arr}
+    )
+    assert v.data.shape == (10, 3)
+    assert v.data.dtype == np.float32
+
+
+def test_from_dict_list_1d_len_not_divisible_raises():
+    """Verify that from_dict raises ValueError when a 1D list's length is not divisible by num_components."""
+    with pytest.raises(ValueError, match="not divisible by num_components"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": "v", "num_components": 3, "data": [1, 2, 3, 4, 5]}
+        )
+
+
+def test_from_dict_list_2d_last_dim_match_ok_and_dtype():
+    """Verify that from_dict works with a 2D list whose last dimension matches num_components and casts dtype."""
+    data = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]  # shape (2,3)
+    v = tvu.VisorVariableUpdate.from_dict(
+        {"type": "POINT", "name": "vec", "num_components": 3, "data": data}
+    )
+    assert v.data.shape == (2, 3)
+    assert v.data.dtype == np.float32
+
+
+def test_from_dict_list_2d_last_dim_mismatch_raises():
+    """Verify that from_dict raises ValueError when a 2D list's last dimension does not match num_components."""
+    data = [[1.0, 2.0], [3.0, 4.0]]  # last dim=2
+    with pytest.raises(ValueError, match="iterator too short"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": "vec", "num_components": 3, "data": data}
+        )
+
+
+@pytest.mark.parametrize("missing_key", ["type", "name", "num_components", "data"])
+def test_from_dict_missing_required_field_raises(missing_key):
+    """Verify that from_dict raises ValueError when a required field is missing."""
+    base = {"type": "POINT", "name": "n", "num_components": 1, "data": [1.0]}
+    base.pop(missing_key)
+    with pytest.raises(ValueError, match=f"Missing required field: {missing_key}"):
+        tvu.VisorVariableUpdate.from_dict(base)
+
+
+def test_from_dict_num_components_not_int_raises():
+    """Verify that from_dict raises ValueError when num_components is not an integer."""
+    with pytest.raises(ValueError, match="num_components must be an integer"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": "n", "num_components": "abc", "data": [1]}
+        )
+
+
+@pytest.mark.parametrize("bad_name", ["", "   ", None])
+def test_from_dict_bad_name_raises(bad_name):
+    """Verify that from_dict raises ValueError when name is empty or None."""
+    with pytest.raises(ValueError, match="name must be non-empty"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": bad_name, "num_components": 1, "data": [1]}
+        )
+
+
+def test_from_dict_data_none_raises():
+    """Verify that from_dict raises ValueError when data is None."""
+    with pytest.raises(ValueError, match="data must be provided"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": "n", "num_components": 1, "data": None}
+        )
+
+
+def test_from_dict_data_string_raises_type_error():
+    """Verify that from_dict raises TypeError when data is a string (not a sequence or ndarray)."""
+    with pytest.raises(TypeError, match="data must be a numpy.ndarray or a sequence"):
+        tvu.VisorVariableUpdate.from_dict(
+            {"type": "POINT", "name": "n", "num_components": 1, "data": "not_valid"}
+        )
diff --git a/tests/unit/vtk/variables/test_visor_variable_info.py b/tests/unit/vtk/variables/test_visor_variable_info.py
new file mode 100644
index 00000000..311540bc
--- /dev/null
+++ b/tests/unit/vtk/variables/test_visor_variable_info.py
@@ -0,0 +1,76 @@
+# python
+import json
+
+import pytest
+from pydantic import ValidationError
+
+from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType
+from ansys.visor.viewer.models.info.visor_variable_info import VisorVariableInfo
+
+
+def _valid_payload():
+    return {
+        "indexForType": 0,
+        "type": "POINT",
+        "name": "velocity",
+        "numComponents": 3,
+        "ranges": [(0.0, 1.0), (1.0, 2.0), (2.0, 5.0)],
+        "magnitudeRange": (0.0, 5.0),
+    }
+
+
+def test_create_with_all_fields():
+    """Verify that a VisorVariableInfo can be created with all fields."""
+    m = VisorVariableInfo(**_valid_payload())
+    assert (m.index_for_type, m.type, m.name, m.num_components) == (
+        0, VisorVtkVariableType.POINT, "velocity", 3
+    )
+    assert m.ranges == [(0.0, 1.0), (1.0, 2.0), (2.0, 5.0)]
+    assert m.magnitude_range == (0.0, 5.0)
+
+
+def test_optional_field_defaults_to_none():
+    """Verify that optional field magnitudeRange defaults to None if not provided."""
+    data = _valid_payload()
+    data.pop("magnitudeRange")
+    m = VisorVariableInfo(**data)
+    assert m.magnitude_range is None
+
+
+@pytest.mark.parametrize(
+    "bad_ranges",
+    [
+        [1.0, 2.0],              # not a list of 2-tuples
+        [(1.0, 2.0, 3.0)],       # wrong tuple length
+        [(None, 1.0)],           # wrong element type
+    ],
+)
+def test_invalid_ranges_raise_validation_error(bad_ranges):
+    """Verify that invalid ranges raise validation error."""
+    data = _valid_payload()
+    data["ranges"] = bad_ranges
+    with pytest.raises(ValidationError):
+        VisorVariableInfo(**data)
+
+
+@pytest.mark.parametrize(
+    "bad_magnitude",
+    [
+        (1.0,),          # wrong length
+        "bad",           # wrong type
+    ],
+)
+def test_invalid_magnitude_range_raises_validation_error(bad_magnitude):
+    """Verify that invalid magnitudeRange raises validation error."""
+    data = _valid_payload()
+    data["magnitudeRange"] = bad_magnitude
+    with pytest.raises(ValidationError):
+        VisorVariableInfo(**data)
+
+
+def test_model_dump_json_shape():
+    """Verify that model_dump_json produces expected JSON shape."""
+    m = VisorVariableInfo(**_valid_payload())
+    back = json.loads(m.model_dump_json(by_alias=True))
+    assert back["ranges"] == [[0.0, 1.0], [1.0, 2.0], [2.0, 5.0]]
+    assert back["magnitudeRange"] == [0.0, 5.0]
diff --git a/tests/unit/vtk/variables/test_visor_variables.py b/tests/unit/vtk/variables/test_visor_variables.py
new file mode 100644
index 00000000..b7943315
--- /dev/null
+++ b/tests/unit/vtk/variables/test_visor_variables.py
@@ -0,0 +1,247 @@
+import os
+import sys
+from dataclasses import dataclass
+from typing import List, Optional, Tuple
+
+import pytest
+
+from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType
+
+# Ensure 'src' layout is importable
+sys.path.insert(0, os.path.abspath("src"))
+
+# Import the module under test
+import ansys.visor.viewer.vtk.variables.visor_variables as tvmod
+
+# ----- Fake external dependencies and VTK-like classes -----
+
+@dataclass
+class FakeVariableInfo:
+    index_for_type: int
+    type: str
+    name: str
+    num_components: int
+    ranges: List[Tuple[float, float]]
+    magnitude_range: Optional[Tuple[float, float]]
+
+
+@dataclass
+class FakeUpdate:
+    name: str
+    num_components: int
+    type: VisorVtkVariableType
+
+
+class FakeDataArray:
+    def __init__(
+        self,
+        name: str,
+        num_components: int,
+        num_tuples: int,
+        component_ranges: List[Tuple[float, float]],
+        magnitude_range: Optional[Tuple[float, float]] = None,
+    ):
+        assert len(component_ranges) == num_components
+        self._name = name
+        self._num_components = num_components
+        self._num_tuples = num_tuples
+        self._component_ranges = component_ranges
+        self._magnitude_range = magnitude_range if magnitude_range is not None else (0.0, 0.0)
+
+    def GetName(self): # noqa: N802
+        return self._name
+
+    def GetNumberOfComponents(self): # noqa: N802
+        return self._num_components
+
+    def GetNumberOfTuples(self):# noqa: N802
+        return self._num_tuples
+
+    def GetRange(self, i: int): # noqa: N802
+        if i < 0:
+            return self._magnitude_range
+        return self._component_ranges[i]
+
+
+class FakeFieldData:
+    def __init__(self, arrays: List[Optional[FakeDataArray]]):
+        self._arrays = arrays
+
+    def GetNumberOfArrays(self): # noqa: N802
+        return len(self._arrays)
+
+    def GetArray(self, i: int): # noqa: N802
+        return self._arrays[i]
+
+
+class FakeDataset:
+    def __init__(self, point_arrays: List[Optional[FakeDataArray]], cell_arrays: List[Optional[FakeDataArray]]):
+        self._point = FakeFieldData(point_arrays)
+        self._cell = FakeFieldData(cell_arrays)
+
+    def GetPointData(self): # noqa: N802
+        return self._point
+
+    def GetCellData(self): # noqa: N802
+        return self._cell
+
+
+# ----- Auto-patch module dependencies for all tests -----
+
+@pytest.fixture(autouse=True)
+def patch_module_dependencies(monkeypatch):
+    """Provides fake external dependencies for the module under test."""
+    # Replace external enum and info class
+    monkeypatch.setattr(tvmod, "VisorVtkVariableType", VisorVtkVariableType, raising=True)
+    monkeypatch.setattr(tvmod, "VisorVariableInfo", FakeVariableInfo, raising=True)
+    # Note: is_composite_dataset was removed from visor_variables โ€” VisorVariables
+    # now always operates on a single leaf block (composite traversal is done at the
+    # VisorDataset level before constructing per-part VisorVariables instances).
+
+
+# ----- Tests for VisorVariable public API -----
+
+def test_visor_variable_from_array_and_conversions():
+    """Verify that VisorVariable.from_array and conversions to dict/info work correctly."""
+    arr = FakeDataArray(
+        name="Velocity",
+        num_components=3,
+        num_tuples=100,
+        component_ranges=[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)],
+        magnitude_range=(0.0, 3.5),
+    )
+
+    var = tvmod.VisorVariable.from_array(index=2, arr=arr, variable_type=VisorVtkVariableType.POINT)
+
+    assert var.index == 2
+    assert var.type is VisorVtkVariableType.POINT
+    assert var.name == "Velocity"
+    assert var.num_components == 3
+    assert var.num_points == 100
+    assert var.ranges == [(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]
+    assert var.magnitude_range == (0.0, 3.5)
+
+    as_dict = var.to_dict()
+    assert as_dict["index"] == 2
+    assert as_dict["type"] == "POINT"
+    assert as_dict["name"] == "Velocity"
+    assert as_dict["num_components"] == 3
+    assert as_dict["num_points"] == 100
+    assert as_dict["ranges"] == [(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]
+    assert as_dict["magnitude_range"] == (0.0, 3.5)
+
+    info = var.to_info()
+    assert isinstance(info, FakeVariableInfo)
+    assert info.index_for_type == 2
+    assert info.type is VisorVtkVariableType.POINT
+    assert info.name == "Velocity"
+    assert info.num_components == 3
+    assert info.ranges == [(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]
+    assert info.magnitude_range == (0.0, 3.5)
+
+
+# ----- Tests for VisorVariables public API -----
+
+def test_visor_variables_init_and_listing():
+    """Verify that VisorVariables initializes correctly and lists variables."""
+    # One point array, two cell arrays with one None to ensure it is skipped
+    p_arr = FakeDataArray("PVar", 1, 10, [(0.0, 10.0)], (0.0, 10.0))
+    c_arr = FakeDataArray("CVar", 2, 5, [(1.0, 2.0), (3.0, 4.0)], (1.0, 4.0))
+    ds = FakeDataset(point_arrays=[p_arr], cell_arrays=[c_arr, None])
+
+    variables = tvmod.VisorVariables(ds)
+
+    all_vars = variables.list()
+    assert len(all_vars) == 2
+    names = {v.name for v in all_vars}
+    assert names == {"PVar", "CVar"}
+
+    # Ensure types and indexes
+    p = next(v for v in all_vars if v.name == "PVar")
+    c = next(v for v in all_vars if v.name == "CVar")
+    assert p.type is VisorVtkVariableType.POINT and p.index == 0
+    assert c.type is VisorVtkVariableType.CELL and c.index == 0
+
+    # list_info returns FakeVariableInfo objects
+    infos = variables.list_info()
+    assert len(infos) == 2
+    assert {i.name for i in infos} == {"PVar", "CVar"}
+    assert {i.type for i in infos} == {VisorVtkVariableType.POINT, VisorVtkVariableType.CELL}
+
+
+def test_visor_variables_reload_replaces_metadata():
+    """Verify that VisorVariables.reload replaces the internal variable metadata."""
+    # Initial dataset
+    p1 = FakeDataArray("P1", 1, 3, [(0.0, 1.0)], (0.0, 1.0))
+    c1 = FakeDataArray("C1", 1, 2, [(2.0, 3.0)], (2.0, 3.0))
+    ds1 = FakeDataset(point_arrays=[p1], cell_arrays=[c1])
+
+    variables = tvmod.VisorVariables(ds1)
+    assert {v.name for v in variables.list()} == {"P1", "C1"}
+
+    # New dataset
+    p2 = FakeDataArray("P2", 2, 4, [(0.0, 5.0), (1.0, 6.0)], (0.0, 6.0))
+    p3 = FakeDataArray("P3", 1, 1, [(-1.0, 1.0)], (-1.0, 1.0))
+    ds2 = FakeDataset(point_arrays=[p2, p3], cell_arrays=[])
+
+    variables.reload(ds2)
+    names_after = {v.name for v in variables.list()}
+    assert names_after == {"P2", "P3"}
+    assert all(v.type is VisorVtkVariableType.POINT for v in variables.list())
+
+
+def test_validate_new_variables_happy_path_and_edge_cases():
+    """Verify that validate_new_variables returns True for valid updates and False for invalid ones."""
+    # Dataset with one POINT and one CELL variable
+    p = FakeDataArray("PVar", 1, 10, [(0.0, 10.0)], (0.0, 10.0))
+    c = FakeDataArray("CVar", 3, 5, [(1.0, 2.0), (3.0, 5.0), (0.0, 4.0)], (0.0, 5.0))
+    ds = FakeDataset(point_arrays=[p], cell_arrays=[c])
+
+    variables = tvmod.VisorVariables(ds)
+
+    # Valid updates
+    updates_valid = [
+        FakeUpdate(name="PVar", num_components=1, type=VisorVtkVariableType.POINT),
+        FakeUpdate(name="CVar", num_components=3, type=VisorVtkVariableType.CELL),
+    ]
+    assert variables.validate_new_variables(updates_valid) is True
+
+    # Empty list is valid
+    assert variables.validate_new_variables([]) is True
+
+    # Unknown name for POINT
+    updates_bad_name = [
+        FakeUpdate(name="Unknown", num_components=1, type=VisorVtkVariableType.POINT),
+    ]
+    assert variables.validate_new_variables(updates_bad_name) is False
+
+    # Known name but wrong component count
+    updates_bad_components = [
+        FakeUpdate(name="PVar", num_components=2, type=VisorVtkVariableType.POINT),
+    ]
+    assert variables.validate_new_variables(updates_bad_components) is False
+
+    # Mixed: one valid, one invalid should be False
+    updates_mixed = [
+        FakeUpdate(name="PVar", num_components=1, type=VisorVtkVariableType.POINT),
+        FakeUpdate(name="CVar", num_components=2, type=VisorVtkVariableType.CELL),
+    ]
+    assert variables.validate_new_variables(updates_mixed) is False
+
+
+def test_init_raises_on_none_dataset():
+    """Verify that VisorVariables raises ValueError if initialized with None."""
+    with pytest.raises(ValueError):
+        tvmod.VisorVariables(None)
+
+
+def test_init_with_composite_dataset_does_not_raise(monkeypatch):
+    """VisorVariables no longer guards against composite datasets.
+    Composite traversal is handled at the VisorDataset level; VisorVariables
+    is always constructed with a single leaf block and just reads point/cell data."""
+    p = FakeDataArray("P", 1, 1, [(0.0, 1.0)], (0.0, 1.0))
+    ds = FakeDataset(point_arrays=[p], cell_arrays=[])
+
+    # Should not raise โ€” VisorVariables now reads whatever dataset it is given
+    tv = tvmod.VisorVariables(ds)
+    assert len(tv.list()) == 1
diff --git a/tests/unit/vtk/widgets/test_orientation_widget.py b/tests/unit/vtk/widgets/test_orientation_widget.py
new file mode 100644
index 00000000..ef3088fc
--- /dev/null
+++ b/tests/unit/vtk/widgets/test_orientation_widget.py
@@ -0,0 +1,71 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ansys.visor.viewer.vtk.widgets.visor_orientation import VisorOrientationWidget
+
+
+@pytest.fixture
+def mock_renderer():
+    """Provides a mock renderer for testing."""
+    return MagicMock()
+
+@pytest.fixture
+def mock_interactor():
+    """Provides a mock interactor for testing."""
+    return MagicMock()
+
+@pytest.fixture
+def mock_widget():
+    """Provides a mock widget for testing."""
+    widget = MagicMock()
+    widget.GetEnabled.return_value = 1
+    widget.GetRepresentation.return_value = MagicMock()
+    return widget
+
+@pytest.fixture
+def mock_representation():
+    """Provides a mock representation for testing."""
+    rep = MagicMock()
+    return rep
+
+def test_init_creates_widget(monkeypatch, mock_renderer, mock_interactor, mock_widget, mock_representation):
+    """Verify that widget is created."""
+    # Patch vtkCameraOrientationWidget to return our mock
+    with patch("ansys.visor.viewer.vtk.widgets.visor_orientation.vtkCameraOrientationWidget", return_value=mock_widget):
+        mock_widget.GetRepresentation.return_value = mock_representation
+        # Should not raise
+        widget = VisorOrientationWidget(mock_renderer, mock_interactor)
+        assert widget._VisorOrientationWidget__widget is mock_widget
+
+def test_is_on_property(monkeypatch, mock_renderer, mock_interactor, mock_widget):
+    """Verify that is_on property returns correct value based on widget's enabled state."""
+    with patch("ansys.visor.viewer.vtk.widgets.visor_orientation.vtkCameraOrientationWidget", return_value=mock_widget):
+        widget = VisorOrientationWidget(mock_renderer, mock_interactor)
+        mock_widget.GetEnabled.return_value = 1
+        assert widget.is_on is True
+        mock_widget.GetEnabled.return_value = 0
+        assert widget.is_on is False
+
+def test_register_with_local_view(monkeypatch, mock_renderer, mock_interactor, mock_widget):
+    """Verify that register_with_local_view method registers widget with local view."""
+    with patch("ansys.visor.viewer.vtk.widgets.visor_orientation.vtkCameraOrientationWidget", return_value=mock_widget):
+        widget = VisorOrientationWidget(mock_renderer, mock_interactor)
+        local_view = MagicMock()
+        local_view.register_vtk_object.return_value = 42
+        widget.register_with_local_view(local_view)
+        assert widget._VisorOrientationWidget__widget_wasm_id == 42
+
+def test_widget_wasm_id_property(monkeypatch, mock_renderer, mock_interactor, mock_widget):
+    """Verify that widget_wasm_id property returns correct value based on widget's enabled state."""
+    with patch("ansys.visor.viewer.vtk.widgets.visor_orientation.vtkCameraOrientationWidget", return_value=mock_widget):
+        widget = VisorOrientationWidget(mock_renderer, mock_interactor)
+        # Not registered yet, should raise
+        with pytest.raises(RuntimeError, match="register_with_local_view\\(\\) must be called before this property is available"):
+            _ = widget.widget_wasm_id
+        # Register and check
+        local_view = MagicMock()
+        local_view.register_vtk_object.return_value = 99
+        widget.register_with_local_view(local_view)
+        assert widget.widget_wasm_id == 99
+
diff --git a/tests/unit/vtk/widgets/test_visor_bounding_box.py b/tests/unit/vtk/widgets/test_visor_bounding_box.py
new file mode 100644
index 00000000..0059ad94
--- /dev/null
+++ b/tests/unit/vtk/widgets/test_visor_bounding_box.py
@@ -0,0 +1,106 @@
+from unittest.mock import MagicMock, create_autospec
+
+import pytest
+from vtkmodules.vtkRenderingCore import vtkCamera
+
+from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphNode
+from ansys.visor.viewer.vtk.widgets.visor_bounding_box import VisorBoundingBox
+
+
+@pytest.fixture
+def mock_camera():
+    """Provides a mock camera instance"""
+    # not actually a mock, but a real vtkCamera instance
+    # VisorBoundingBox needs a real vtkCamera for SetCamera calls.
+    # This cannot be fully mocked without breaking internal VTK behavior.
+    return vtkCamera()
+
+@pytest.fixture
+def bbox(mock_camera):
+    """
+    Creates a VisorBoundingBox with:
+      - Real vtkCamera (needed)
+      - Mocked VTK dependencies (cube, outline, mapper, actor, text, axes)
+
+    Using MonkeyPatch to replace the VTK classes with MagicMocks.
+    This prevents actual rendering, keeps tests fast and isolated.
+    """
+    with pytest.MonkeyPatch.context() as m:
+        m.setattr("vtkmodules.vtkFiltersSources.vtkCubeSource", lambda: MagicMock())
+        m.setattr("vtkmodules.vtkFiltersModeling.vtkOutlineFilter", lambda: MagicMock())
+        m.setattr("vtkmodules.vtkRenderingCore.vtkPolyDataMapper", lambda: MagicMock())
+        m.setattr("vtkmodules.vtkRenderingCore.vtkActor", lambda: MagicMock())
+        m.setattr("vtkmodules.vtkRenderingCore.vtkTextProperty", lambda: MagicMock())
+        m.setattr("vtkmodules.vtkRenderingAnnotation.vtkCubeAxesActor2D", lambda: MagicMock())
+        return VisorBoundingBox(mock_camera, node_count=0)
+
+# ---------------------------
+# Tests
+# ---------------------------
+
+def test_initial_state_hidden(bbox):
+    """Verify actors can be shown/hidden without errors"""
+    bbox.show()
+    bbox.hide()
+    # No error should be raised
+
+def test_update_node_count(bbox):
+    """Updating internal node count doesn't raise errors"""
+    bbox.update_node_count(5)
+    # Should update internal node_count (no error)
+
+def test_toggle_visibility(bbox):
+    """Toggles visibility correctly"""
+    bbox.update_node_count(1)
+    bbox.show()
+    bbox.toggle()
+    bbox.toggle()
+    # Should toggle visibility
+
+def test_update_outline_bounds(bbox):
+    """Updates outline bounds"""
+    bbox.update_bounds([0, 1, 0, 1, 0, 1])
+
+def test_update_outline_bounds_from_nodes(bbox):
+    """Calculates bounds from multiple scene nodes"""
+    node1 = create_autospec(VisorSceneGraphNode, instance=True)
+    node1.bounds = [0, 1, 0, 1, 0, 1]
+    node2 = create_autospec(VisorSceneGraphNode, instance=True)
+    node2.bounds = [2, 3, 2, 3, 2, 3]
+    nodes = {1: node1, 2: node2}
+    bbox.update_bounds_from_nodes(nodes)
+    # Should update bounds to [0, 3, 0, 3, 0, 3]
+
+def test_update_outline_bounds_from_nodes_empty(bbox):
+    """Handles empty node dict correctly (hides actors)"""
+    bbox.update_bounds_from_nodes({})
+    # Should hide actors
+
+def test_attach_and_detach_renderer(bbox):
+    """Attaches and detaches renderer, verifies calls"""
+    renderer = MagicMock()
+    bbox.attach_to_renderer(renderer)
+    bbox.detach_from_renderer(renderer)
+    renderer.AddActor.assert_called()
+    renderer.AddViewProp.assert_called()
+    renderer.RemoveActor.assert_called()
+    renderer.RemoveViewProp.assert_called()
+
+def test_wasm_id_properties_raise_before_register(bbox):
+    """Accessing WASM IDs before registering raises errors"""
+    with pytest.raises(RuntimeError, match="register_with_local_view"):
+        _ = bbox.box_algorithm_wasm_id
+    with pytest.raises(RuntimeError, match="register_with_local_view"):
+        _ = bbox.outline_wasm_actor_id
+    with pytest.raises(RuntimeError, match="register_with_local_view"):
+        _ = bbox.axes_wasm_actor_id
+
+def test_register_with_local_view_sets_wasm_ids(bbox):
+    """Registering with a local view sets WASM IDs correctly"""
+    local_view = MagicMock()
+    local_view.get_wasm_id.side_effect = [101, 102, 103]
+    bbox.register_with_local_view(local_view)
+    assert bbox.box_algorithm_wasm_id == 101
+    assert bbox.outline_wasm_actor_id == 102
+    assert bbox.axes_wasm_actor_id == 103
+
diff --git a/tests/unit/vtk/widgets/test_visor_cross_section_widget.py b/tests/unit/vtk/widgets/test_visor_cross_section_widget.py
new file mode 100644
index 00000000..af754edf
--- /dev/null
+++ b/tests/unit/vtk/widgets/test_visor_cross_section_widget.py
@@ -0,0 +1,199 @@
+import pytest
+
+from ansys.visor.viewer.vtk.widgets.visor_cross_section import VisorCrossSectionWidget
+
+
+@pytest.fixture
+def mocks(mocker):
+    """Provides mocks for VTK classes used in VisorCrossSectionWidget."""
+    # Patch VTK classes in the module under test (must use its real import path)
+    plane = mocker.patch('ansys.visor.viewer.vtk.widgets.visor_cross_section.vtkPlane').return_value
+    rep = mocker.patch('ansys.visor.viewer.vtk.widgets.visor_cross_section.vtkImplicitPlaneRepresentation').return_value
+    widget = mocker.patch('ansys.visor.viewer.vtk.widgets.visor_cross_section.vtkImplicitPlaneWidget2').return_value
+    algo = mocker.patch('ansys.visor.viewer.vtk.widgets.visor_cross_section.vtkAlgorithm').return_value
+    clipper = mocker.patch('ansys.visor.viewer.vtk.widgets.visor_cross_section.vtkClipPolyData').return_value
+    mocker.patch('ansys.visor.viewer.vtk.widgets.visor_cross_section.logger')
+    localview = mocker.patch('ansys.visor.viewer.vtk.widgets.visor_cross_section.LocalView').return_value
+
+    widget.GetEnabled.return_value = 1
+    rep.PlaceWidget = mocker.Mock()
+    rep.SetNormal = mocker.Mock()
+    rep.SetOrigin = mocker.Mock()
+    rep.SetPlaceFactor = mocker.Mock()
+    rep.SetOutlineTranslation = mocker.Mock()
+    widget.On = mocker.Mock()
+    widget.Off = mocker.Mock()
+    widget.SetInteractor = mocker.Mock()
+    widget.SetRepresentation = mocker.Mock()
+    plane.SetNormal = mocker.Mock()
+    plane.SetOrigin = mocker.Mock()
+    plane.normal = (0, 0, 1)
+    algo.GetOutputPort = mocker.Mock(return_value='output_port')
+    clipper.SetClipFunction = mocker.Mock()
+    clipper.SetInsideOut = mocker.Mock()
+    clipper.SetInputConnection = mocker.Mock()
+    localview.register_vtk_object = mocker.Mock(side_effect=[1, 2, 3])
+    interactor = mocker.Mock()
+    return {
+        'plane': plane,
+        'rep': rep,
+        'widget': widget,
+        'algo': algo,
+        'clipper': clipper,
+        'localview': localview,
+        'interactor': interactor
+    }
+
+def test_initialization(mocks):
+    """Verify that VisorCrossSectionWidget initializes correctly with the provided interactor."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    assert widget._plane is not None
+    assert widget._plane_representation is not None
+    assert widget._plane_widget is not None
+    assert callable(widget._algorithm_filter)
+    assert widget._place_factor == 1.25
+    assert widget._inside_out is False
+    assert widget._plane_wasm_id is None
+    assert widget._plane_widget_wasm_id is None
+    assert widget._plane_representation_wasm_id is None
+
+def test_update_bounds_calls_methods(mocks):
+    """Verify that update_bounds calls the appropriate methods on the representation and plane."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    bounds = [1, 2, 3, 4, 5, 6]
+    widget.update_bounds(bounds)
+    assert mocks['rep'].PlaceWidget.call_count >= 1
+    mocks['rep'].PlaceWidget.assert_any_call(bounds)
+    mocks['rep'].SetOrigin.assert_called()
+    mocks['rep'].SetNormal.assert_called()
+    mocks['plane'].SetOrigin.assert_called()
+    mocks['plane'].SetNormal.assert_called()
+
+def test_plane_property(mocks):
+    """Verify that the plane property returns the internal plane object."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    assert widget.plane is widget._plane
+
+def test_algorithm_filter_property(mocks):
+    """Verify that the algorithm_filter property returns the internal algorithm filter object."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    assert widget.algorithm_filter is widget._algorithm_filter
+
+def test_plane_wasm_id_property_raises(mocks):
+    """Verify that accessing plane_wasm_id before registration raises a RuntimeError."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    with pytest.raises(RuntimeError):
+        _ = widget.plane_wasm_id
+
+def test_plane_widget_wasm_id_property_raises(mocks):
+    """Verify that accessing plane_widget_wasm_id before registration raises a RuntimeError."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    with pytest.raises(RuntimeError):
+        _ = widget.plane_widget_wasm_id
+
+def test_plane_representation_wasm_id_property_raises(mocks):
+    """Verify that accessing plane_representation_wasm_id before registration raises a RuntimeError."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    with pytest.raises(RuntimeError):
+        _ = widget.plane_representation_wasm_id
+
+def test_is_on_property(mocks):
+    """Verify that the is_on property returns the correct value."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    assert widget.is_on
+    widget._plane_widget.GetEnabled.return_value = 0
+    assert not widget.is_on
+
+def test_register_with_local_view_sets_ids(mocks):
+    """Verify that register_with_local_view sets the correct local view."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    widget.register_with_local_view(mocks['localview'])
+    assert widget._plane_wasm_id == 1
+    assert widget._plane_widget_wasm_id == 2
+    assert widget._plane_representation_wasm_id == 3
+
+def test_set_normal(mocks):
+    """Verify that set_normal sets the correct value."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    normal = (1.0, 0.0, 0.0)
+    widget.set_normal(normal)
+    mocks['rep'].SetNormal.assert_called_with(*normal)
+    mocks['plane'].SetNormal.assert_called_with(*normal)
+
+def test_set_origin(mocks):
+    """Verify that set_origin sets the correct value."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    origin = (0.0, 1.0, 2.0)
+    widget.set_origin(origin)
+    mocks['rep'].SetOrigin.assert_called_with(*origin)
+    mocks['plane'].SetOrigin.assert_called_with(*origin)
+
+def test_toggle_on_off(mocks):
+    """Verify that toggle_on_off sets the correct value."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    widget._plane_widget.GetEnabled.return_value = 1
+    widget.toggle_on_off()
+    mocks['widget'].Off.assert_called()
+    widget._plane_widget.GetEnabled.return_value = 0
+    widget.toggle_on_off()
+    mocks['widget'].On.assert_called()
+
+def test_turn_on(mocks):
+    """Verify that turn_on sets the correct value."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    widget.turn_on()
+    mocks['widget'].On.assert_called()
+
+def test_turn_off(mocks):
+    """Verify that turn_off sets the correct value."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    widget.turn_off()
+    mocks['widget'].Off.assert_called()
+
+def test_update_bounds_missing_plane_representation(mocks):
+    """Verify that update_bounds_missing_plane_representation raises a RuntimeError."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    del widget._plane_representation
+    bounds = [1, 2, 3, 4, 5, 6]
+    with pytest.raises(RuntimeError, match="_plane_representation must be initialized before calling update_bounds\(\)"):
+        widget.update_bounds(bounds)
+
+def test_plane_wasm_id_property_happy(mocks):
+    """Verify the plane_wasm_id property happy case."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    widget.register_with_local_view(mocks['localview'])
+    assert widget.plane_wasm_id == 1
+
+def test_plane_widget_wasm_id_property_happy(mocks):
+    """Verify the PlaneWidth property happy case."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    widget.register_with_local_view(mocks['localview'])
+    assert widget.plane_widget_wasm_id == 2
+
+def test_plane_representation_wasm_id_property_happy(mocks):
+    """Verify the PlaneRepresentationId property happy case."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    widget.register_with_local_view(mocks['localview'])
+    assert widget.plane_representation_wasm_id == 3
+
+def test_algorithm_filter_hits_clipper_code(mocks):
+    """Verify that the algorithm filter hits the clipper code."""
+    widget = VisorCrossSectionWidget(mocks['interactor'])
+    # Use the public algorithm_filter property to get the clipper
+    clipper = widget.algorithm_filter(mocks['algo'])
+    # Check that the returned object is the mocked clipper
+    assert clipper is mocks['clipper']
+    # Check that the clipper was configured correctly
+    mocks['clipper'].SetClipFunction.assert_called_with(widget._plane)
+    mocks['clipper'].SetInsideOut.assert_called_with(1 if widget._inside_out else 0)
+    mocks['clipper'].SetInputConnection.assert_called_with(mocks['algo'].GetOutputPort())
+
+def test_init_runtime_error_patch(mocks, mocker):
+    """Verify that init_runtime_error_patch raises a RuntimeError."""
+    # Patch the private method used in __init__ to raise RuntimeError
+    mocker.patch(
+        'ansys.visor.viewer.vtk.widgets.visor_cross_section.VisorCrossSectionWidget._initialize_plane',
+        side_effect=RuntimeError("fail"),
+    )
+    with pytest.raises(RuntimeError, match="could not create cross-section widget: fail"):
+        VisorCrossSectionWidget(mocks['interactor'])
diff --git a/tools/Makefile b/tools/Makefile
new file mode 100644
index 00000000..a8956c2b
--- /dev/null
+++ b/tools/Makefile
@@ -0,0 +1,23 @@
+FRONTEND_DIR ?= src/ansys/visor/visor-client
+FRONTEND_BUILD_DIR ?= $(FRONTEND_DIR)/dist
+BACKEND_STATIC_DIR ?= src/ansys/visor/viewer/client_bundle
+WASM_OUT_DIR ?= $(BACKEND_STATIC_DIR)/wasm
+
+.PHONY: clean clean-all _clean_python _clean_frontend _clean_static
+
+_clean_python:
+	find . -type d -name "__pycache__" -prune -exec rm -rf {} +
+	find . -type f -name "*.py[co]" -delete
+	rm -rf build/ dist/ *.egg-info .pytest_cache coverage-html
+
+_clean_frontend:
+	rm -rf $(FRONTEND_BUILD_DIR) $(FRONTEND_DIR)/build $(FRONTEND_DIR)/.vite $(FRONTEND_DIR)/.cache $(FRONTEND_DIR)/.parcel-cache
+
+_clean_static:
+	rm -rf $(WASM_OUT_DIR) $(BACKEND_STATIC_DIR)/assets $(BACKEND_STATIC_DIR)/manifest.* $(BACKEND_STATIC_DIR)/_temp tmp temp
+
+clean:
+	@$(MAKE) -s _clean_python _clean_frontend _clean_static
+
+clean-all: clean
+	@rm -rf $(FRONTEND_DIR)/node_modules
diff --git a/tools/awc_icon_generator/AWC-FONTS-ICONS-README.md b/tools/awc_icon_generator/AWC-FONTS-ICONS-README.md
new file mode 100644
index 00000000..daeabba6
--- /dev/null
+++ b/tools/awc_icon_generator/AWC-FONTS-ICONS-README.md
@@ -0,0 +1,83 @@
+# AWC Fonts & Icon Library Generator Readme
+
+## Overview
+
+For the corresponding GitHub discussion, please visit https://github.com/ansys-internal/theia/issues/231
+
+In order to make use of AWC fonts and icons without installing the entire AWC React npm package, we need to import the static assets that the AWC React npm package uses. These assets include the following:
+
+- The default AWC UI font (Source Sans 3) and its corresponding CSS font-family stack
+- AWC icon SVGs
+
+Lastly, due to the large number of AWC icon SVG files, there was a need for automating the creation of a JavaScript AWC icon library based on a given list of icon names. This automation resulted in the `awc-icon-generator.html` file found in the Visor repository.
+
+The following sections elaborate on obtaining the AWC font and its font stack, as well as the design behind the `awc-icon-generator.html` file found in the Visor repository.
+
+## Default AWC UI Font
+
+As of 2025-04-17, the font stack for AWC UI text is the following:
+
+```css
+.awc-text {
+    font-family: "Source Sans 3",
+    -apple-system,
+    BlinkMacSystemFont,
+    "Segoe UI",
+    Roboto,
+    "Noto Sans",
+    Ubuntu,
+    Cantarell,
+    "Helvetica Neue",
+    sans-serif,
+    "Apple Color Emoji",
+    "Segoe UI Emoji",
+    "Segoe UI Symbol",
+    "Noto Color Emoji";
+}
+```
+
+As you can see, the primary font is Source Sans 3. This font stack can be found in the following CSS file (after installing the AWC React npm package):
+
+**@ansys\awc-react\styles\themes\css\default.css**
+
+Alternatively, the easiest way to get this font stack is to visit https://prerelease.awc.ansys.com/#/d/icons, open your browser's F12 developer tools, and inspect an element that uses the font using the "DOM and Style Inspector" to see the element's font-family.
+
+Acquiring the "Source Sans 3" font is straightforward. Source Sans 3 can be downloaded from Google Fonts at https://fonts.google.com/specimen/Source+Sans+3.
+
+## AWC Icon SVGs
+
+The actual SVG markup that each AWC icon uses is not included in the AWC npm package. When using the AWC npm package in your application, your frontend will actually make a request to an `amazonaws` url (as of 2025-04-17) to download each icon on an as-needed basis.
+
+The list of icons available at that url (minus the .svg extension) can be found in the following TypeScript file after installing the AWC package:
+
+**@ansys\awc-react\core\types\icon.enum.d.ts**
+
+Note that the list of icons found in the file above does not include each icon's url, rather only its name (minus the .svg extension). In order to get the full url for each icon, we must know the AWC icon base url. The AWC icon base url can be found in the following JavaScript file:
+
+**@ansys\awc-react\types\awc.js**
+
+In that file, base url string is set to a variable that looks like this:
+
+```javascript
+AWC.BASE_ICON_ROUTE = 'https://ansys-web-components.s3.eu-west-2.amazonaws.com/os-icons/v3/';
+```
+
+**So, if you wanted to download the SVG for the "add-circle" icon, the url would be the following:**
+
+`https://ansys-web-components.s3.eu-west-2.amazonaws.com/os-icons/v3/add-circle.svg`
+
+The easiest way to get this icon list is to just
+visit https://prerelease.awc.ansys.com/#/d/icons, open your browser's F12 developer tools,
+go to the network tab, and see where all the icon SVGs on the page are being
+downloaded from. This method saves you from having to install the entire AWC npm package
+just to get the base url.
+
+## AWC Icon Generator (awc-icon-generator.html)
+
+Traditionally, when websites used icon packs, the website would deploy a single .woff font that included each icon as a glyph. In AWC however, each icon is an SVG element. This allows for more flexibility with each icon, such as being able to give different parts of the same icon different colors. This was not possible with the traditional icon font packs. However, the tradeoff with SVGs is that there is a little more setup required to deploy them than there is with a font pack. The way AWC typically renders icons on the frontend is by making a request for each icon's SVG from an `amazonaws` URL.
+
+Since we wanted to use these AWC icons without installing the AWC package, we needed to download all the SVGs and serve them locally. This way, if the icon URLs became obsolete, it would not affect our application.
+
+Visor inserts AWC icons into React components (or anywhere in JavaScript for that matter) by making use of a dynamically-created `AwcIcons.js` file. This JavaScript library is created by the `awc-icon-generator.html` page, which can actually be run independent of Visor.
+
+The `awc-icon-generator.html` page generates JavaScript inside a <textarea> input, that can be copy-pasted into the `AwcIcons.js` file any time AWC updates its icon collection.
\ No newline at end of file
diff --git a/tools/awc_icon_generator/awc-icon-generator.html b/tools/awc_icon_generator/awc-icon-generator.html
new file mode 100644
index 00000000..1746a3ce
--- /dev/null
+++ b/tools/awc_icon_generator/awc-icon-generator.html
@@ -0,0 +1,822 @@
+
+
+
+    
+    AWC Icons
+    
+
+
+
+

AWC Icon Library Generator

+
+
+ This page loops through a list of AWC icon SVG file names, downloads each icon's SVG,
+ and dynamically builds a JavaScript object with methods that return each icon as an HTMLElement object.

+ For information on using this JavaScript generator, either see the readme at

+ [Visor Project Root]\src\ansys\visor\visor-client\utils\tools\AWC-FONTS-ICONS-README.md

+ or see the GitHub discussion here. +
+
+ +
+
+ +
+
+ + + \ No newline at end of file