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]: "
+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 : [: ...]
+#
+# 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 .
+# "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 : [: ...]" >&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 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: _LEGAL_NOTICE[_].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 .
+
+
\ 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
+
+
+
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 `_ 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) `_
+ to provide 3D visualization capabilities.
+* `Ansys Dynamic Reporting `_
+ to support dynamic reporting workflows.
+* `PyAnsys Visualization Interface Tool `_
+ 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 `_
+in the Visor documentation.
+
+
+Quick start
+-----------
+
+For a basic usage example, see
+`Quick start `_
+in the Visor documentation.
+
+
+Development
+-----------
+
+To set up your development environment, see the `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 `_
+ for API usage examples.
+ * See `API reference `_
+ for details about the Python API.
+
+* **Visor HTTP service**
+
+ * Manage multiple Visor instances through the Visor HTTP API.
+ * Integrate with `PIM (Product Instance Management) `_
+ to manage Visor instances in SAF apps.
+ * See `Visor HTTP service `_
+ for usage examples.
+ * See `HTTP API reference `_
+ 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 `_
+ for details.
+
+For advanced usage and configuration options, see the following Visor documentation:
+
+`User guide `_ and
+`Examples `_ 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
+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
+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
+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
+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: https://github.com/ansys-internal/ansys-web-components/issues/2156 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
+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: https://github.com/ansys-internal/ansys-web-components/issues/2156 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= 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.
+
+
+
+
+## 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
+โ โ โ
+โ โ โโ [datasetId] : VisorDatasetState
+โ โ โโ id : string
+โ โ โโ partStates : Record
+โ โ โ
+โ โ โโ [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
+โ โ
+โ โโ [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
+ โโ name : string
+ โโ isGroupNode : true
+ โโ isActorNode : false
+ โโ nodeType : "root"
+ โโ diffuseColor : [number, number, number]
+ โโ bounds : number[]
+ โ
+ โโ children : Array
+ โ
+ โโ DatasetNode (vtkMultiBlockDataSet)
+ โโ id : number
+ โโ wasmActorId : number
+ โโ wasmPropertyId : number
+ โโ wasmMapperId : number
+ โโ dataArrays : Array
+ โโ name : string
+ โโ isGroupNode : true
+ โโ isActorNode : false
+ โโ nodeType : "vtkMultiBlockDataSet"
+ โโ diffuseColor : [number, number, number]
+ โโ bounds : number[]
+ โ
+ โโ children : Array
+ โ
+ โโ PartNode (vtkPolyData)
+ โโ id : number
+ โโ wasmActorId : number
+ โโ wasmPropertyId : number
+ โโ wasmMapperId : number
+ โโ dataArrays : Array
+ โโ 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
+ `_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)
+ - `_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
+ `_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 `_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 `_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 `_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 `_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.)
+
+
+
+
+***
+## 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 `_ 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 `_ 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) `_ 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 `_ 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 `_:
+
+.. 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
+
+ Prerequisites
+
+ 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
+
+ Installation
+
+ Install the VISOR package.
+
+ .. grid-item-card:: :material-outlined:`rocket_launch;2em`
+ :link-type: doc
+ :link: /getting_started/quick_start
+
+ .. raw:: html
+
+ Quick start
+
+ 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
+
+ Known limitations
+
+ See known limitations of the VISOR release.
+
+
+ .. grid-item-card:: :material-outlined:`settings;2em`
+ :link-type: doc
+ :link: /getting_started/configure_visor
+
+ .. raw:: html
+
+ VISOR configuration
+
+ 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 `_
+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 `_
+ - `Node.js & npm `_
+
+#. 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 `_, or follow the
+`Python installation instructions `_
+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) `_
+ to provide 3D visualization capabilities.
+* `Ansys Dynamic Reporting `_
+ to support dynamic reporting workflows.
+* `PyAnsys Visualization Interface Tool `_
+ 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 `_ 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
+
+ Getting started
+
+ 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
+
+ User guide
+
+ 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
+
+ API reference
+
+ 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
+
+ HTTP API reference
+
+ 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
+
+ Examples
+
+ 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
+
+ Supply an input file
+
+ Learn which input formats VISOR supports.
+
+ .. grid-item-card:: :material-outlined:`description;2em`
+ :link-type: doc
+ :link: /user_guide/launching_visor/metadata
+
+ .. raw:: html
+
+ Use metadata
+
+ 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
+
+ Launch VISOR using the Python API
+
+ 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
+
+ Launch VISOR using the HTTP API service
+
+ 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
+
+ Use the VISOR Dash component
+
+ 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 `_).
+
+.. 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 `_ 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": "/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 via VISOR CLI
+ visor-cli instance remove
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
+
+ Add, list, and remove datasets in VISOR
+
+ 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
+
+ Update variable values in a VISOR dataset
+
+ 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 `
+ Learn how to use the VISOR rendering window.
+
+- :ref:`Lower horizontal bar `
+ Learn how to use the controls in the lower horizontal bar.
+
+- :ref:`Part list `
+ Learn how to use the part list.
+
+- :ref:`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 ``